mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-620: Reset password screen (#871)
This commit is contained in:
parent
94f532b9d2
commit
5fffd4e3e2
42 changed files with 1795 additions and 55 deletions
|
@ -5,7 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
||||||
|
@ -26,6 +26,6 @@ interface AccountsApi {
|
||||||
|
|
||||||
@POST("/two-factor/send-email-login")
|
@POST("/two-factor/send-email-login")
|
||||||
suspend fun resendVerificationCodeEmail(
|
suspend fun resendVerificationCodeEmail(
|
||||||
@Body body: ResendEmailJsonRequest,
|
@Body body: ResendEmailRequestJson,
|
||||||
): Result<Unit>
|
): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||||
|
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRequestJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.HTTP
|
import retrofit2.http.HTTP
|
||||||
|
|
||||||
|
@ -8,10 +9,15 @@ import retrofit2.http.HTTP
|
||||||
* Defines raw calls under the /accounts API with authentication applied.
|
* Defines raw calls under the /accounts API with authentication applied.
|
||||||
*/
|
*/
|
||||||
interface AuthenticatedAccountsApi {
|
interface AuthenticatedAccountsApi {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the current account.
|
* Deletes the current account.
|
||||||
*/
|
*/
|
||||||
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
|
@HTTP(method = "DELETE", path = "/accounts", hasBody = true)
|
||||||
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
|
suspend fun deleteAccount(@Body body: DeleteAccountRequestJson): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the password.
|
||||||
|
*/
|
||||||
|
@HTTP(method = "POST", path = "/accounts/password", hasBody = true)
|
||||||
|
suspend fun resetPassword(@Body body: ResetPasswordRequestJson): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import kotlinx.serialization.Serializable
|
||||||
* @property ssoToken The sso token, if the user is logging in via single sign on.
|
* @property ssoToken The sso token, if the user is logging in via single sign on.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ResendEmailJsonRequest(
|
data class ResendEmailRequestJson(
|
||||||
@SerialName("DeviceIdentifier")
|
@SerialName("DeviceIdentifier")
|
||||||
val deviceIdentifier: String,
|
val deviceIdentifier: String,
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for resetting the password.
|
||||||
|
*
|
||||||
|
* @param currentPasswordHash The hash of the user's current password.
|
||||||
|
* @param newPasswordHash The hash of the user's new password.
|
||||||
|
* @param passwordHint The hint for the master password (nullable).
|
||||||
|
* @param key The user key for the request (encrypted).
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class ResetPasswordRequestJson(
|
||||||
|
@SerialName("masterPasswordHash")
|
||||||
|
val currentPasswordHash: String,
|
||||||
|
|
||||||
|
@SerialName("newMasterPasswordHash")
|
||||||
|
val newPasswordHash: String,
|
||||||
|
|
||||||
|
@SerialName("masterPasswordHint")
|
||||||
|
val passwordHint: String?,
|
||||||
|
|
||||||
|
@SerialName("Key")
|
||||||
|
val key: String,
|
||||||
|
)
|
|
@ -4,7 +4,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an API for querying accounts endpoints.
|
* Provides an API for querying accounts endpoints.
|
||||||
|
@ -34,5 +35,10 @@ interface AccountsService {
|
||||||
/**
|
/**
|
||||||
* Resend the email with the two-factor verification code.
|
* Resend the email with the two-factor verification code.
|
||||||
*/
|
*/
|
||||||
suspend fun resendVerificationCodeEmail(body: ResendEmailJsonRequest): Result<Unit>
|
suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the password.
|
||||||
|
*/
|
||||||
|
suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit>
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJso
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
@ -60,6 +61,9 @@ class AccountsServiceImpl constructor(
|
||||||
?: throw throwable
|
?: throw throwable
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resendVerificationCodeEmail(body: ResendEmailJsonRequest): Result<Unit> =
|
override suspend fun resendVerificationCodeEmail(body: ResendEmailRequestJson): Result<Unit> =
|
||||||
accountsApi.resendVerificationCodeEmail(body = body)
|
accountsApi.resendVerificationCodeEmail(body = body)
|
||||||
|
|
||||||
|
override suspend fun resetPassword(body: ResetPasswordRequestJson): Result<Unit> =
|
||||||
|
authenticatedAccountsApi.resetPassword(body = body)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
|
@ -78,6 +79,11 @@ interface AuthRepository : AuthenticatorProvider {
|
||||||
*/
|
*/
|
||||||
var hasPendingAccountAddition: Boolean
|
var hasPendingAccountAddition: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the cached password policies for the current user.
|
||||||
|
*/
|
||||||
|
val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
* Clears the pending deletion state that occurs when the an account is successfully deleted.
|
||||||
*/
|
*/
|
||||||
|
@ -159,6 +165,16 @@ interface AuthRepository : AuthenticatorProvider {
|
||||||
email: String,
|
email: String,
|
||||||
): PasswordHintResult
|
): PasswordHintResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the users password from the [currentPassword] to the [newPassword] and
|
||||||
|
* optional [passwordHint].
|
||||||
|
*/
|
||||||
|
suspend fun resetPassword(
|
||||||
|
currentPassword: String,
|
||||||
|
newPassword: String,
|
||||||
|
passwordHint: String?,
|
||||||
|
): ResetPasswordResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the value of [captchaTokenResultFlow].
|
* Set the value of [captchaTokenResultFlow].
|
||||||
*/
|
*/
|
||||||
|
@ -230,10 +246,8 @@ interface AuthRepository : AuthenticatorProvider {
|
||||||
suspend fun validatePassword(password: String): ValidatePasswordResult
|
suspend fun validatePassword(password: String): ValidatePasswordResult
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the given [password] against a MasterPassword [policy].
|
* Validates the given [password] against the master password
|
||||||
|
* policies for the current user.
|
||||||
*/
|
*/
|
||||||
suspend fun validatePasswordAgainstPolicy(
|
suspend fun validatePasswordAgainstPolicies(password: String): Boolean
|
||||||
password: String,
|
|
||||||
policy: PolicyInformation.MasterPassword,
|
|
||||||
): Boolean
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||||
|
@ -43,6 +44,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
import com.x8bit.bitwarden.data.auth.repository.model.UserFingerprintResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
@ -121,7 +123,7 @@ class AuthRepositoryImpl(
|
||||||
/**
|
/**
|
||||||
* The information necessary to resend the verification code email for two-factor login.
|
* The information necessary to resend the verification code email for two-factor login.
|
||||||
*/
|
*/
|
||||||
private var resendEmailJsonRequest: ResendEmailJsonRequest? = null
|
private var resendEmailRequestJson: ResendEmailRequestJson? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The password that needs to be checked against any organization policies before
|
* The password that needs to be checked against any organization policies before
|
||||||
|
@ -218,6 +220,15 @@ class AuthRepositoryImpl(
|
||||||
override var hasPendingAccountAddition: Boolean
|
override var hasPendingAccountAddition: Boolean
|
||||||
by mutableHasPendingAccountAdditionStateFlow::value
|
by mutableHasPendingAccountAdditionStateFlow::value
|
||||||
|
|
||||||
|
override val passwordPolicies: List<PolicyInformation.MasterPassword>
|
||||||
|
get() = activeUserId?.let { userId ->
|
||||||
|
authDiskSource
|
||||||
|
.getPolicies(userId)
|
||||||
|
?.filter { it.type == PolicyTypeJson.MASTER_PASSWORD && it.isEnabled }
|
||||||
|
?.mapNotNull { it.policyInformation as? PolicyInformation.MasterPassword }
|
||||||
|
.orEmpty()
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pushManager
|
pushManager
|
||||||
.syncOrgKeysFlow
|
.syncOrgKeysFlow
|
||||||
|
@ -239,6 +250,10 @@ class AuthRepositoryImpl(
|
||||||
val userId = activeUserId ?: return@onEach
|
val userId = activeUserId ?: return@onEach
|
||||||
if (passwordPassesPolicies(policies)) {
|
if (passwordPassesPolicies(policies)) {
|
||||||
vaultRepository.completeUnlock(userId = userId)
|
vaultRepository.completeUnlock(userId = userId)
|
||||||
|
storeUserResetPasswordReason(
|
||||||
|
userId = userId,
|
||||||
|
reason = null,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
storeUserResetPasswordReason(
|
storeUserResetPasswordReason(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
|
@ -363,7 +378,7 @@ class AuthRepositoryImpl(
|
||||||
// Cache the data necessary for the remaining two-factor auth flow.
|
// Cache the data necessary for the remaining two-factor auth flow.
|
||||||
identityTokenAuthModel = authModel
|
identityTokenAuthModel = authModel
|
||||||
twoFactorResponse = loginResponse
|
twoFactorResponse = loginResponse
|
||||||
resendEmailJsonRequest = ResendEmailJsonRequest(
|
resendEmailRequestJson = ResendEmailRequestJson(
|
||||||
deviceIdentifier = authDiskSource.uniqueAppId,
|
deviceIdentifier = authDiskSource.uniqueAppId,
|
||||||
email = email,
|
email = email,
|
||||||
passwordHash = authModel.password,
|
passwordHash = authModel.password,
|
||||||
|
@ -399,7 +414,7 @@ class AuthRepositoryImpl(
|
||||||
// Remove any cached data after successfully logging in.
|
// Remove any cached data after successfully logging in.
|
||||||
identityTokenAuthModel = null
|
identityTokenAuthModel = null
|
||||||
twoFactorResponse = null
|
twoFactorResponse = null
|
||||||
resendEmailJsonRequest = null
|
resendEmailRequestJson = null
|
||||||
|
|
||||||
// Attempt to unlock the vault if possible.
|
// Attempt to unlock the vault if possible.
|
||||||
password?.let {
|
password?.let {
|
||||||
|
@ -493,7 +508,7 @@ class AuthRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resendVerificationCodeEmail(): ResendEmailResult =
|
override suspend fun resendVerificationCodeEmail(): ResendEmailResult =
|
||||||
resendEmailJsonRequest?.let { jsonRequest ->
|
resendEmailRequestJson?.let { jsonRequest ->
|
||||||
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
accountsService.resendVerificationCodeEmail(body = jsonRequest).fold(
|
||||||
onFailure = { ResendEmailResult.Error(message = it.message) },
|
onFailure = { ResendEmailResult.Error(message = it.message) },
|
||||||
onSuccess = { ResendEmailResult.Success },
|
onSuccess = { ResendEmailResult.Success },
|
||||||
|
@ -628,6 +643,76 @@ class AuthRepositoryImpl(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ReturnCount")
|
||||||
|
override suspend fun resetPassword(
|
||||||
|
currentPassword: String,
|
||||||
|
newPassword: String,
|
||||||
|
passwordHint: String?,
|
||||||
|
): ResetPasswordResult {
|
||||||
|
val activeAccount = authDiskSource
|
||||||
|
.userState
|
||||||
|
?.activeAccount
|
||||||
|
?: return ResetPasswordResult.Error
|
||||||
|
val currentPasswordHash = authSdkSource
|
||||||
|
.hashPassword(
|
||||||
|
email = activeAccount.profile.email,
|
||||||
|
password = currentPassword,
|
||||||
|
kdf = activeAccount.profile.toSdkParams(),
|
||||||
|
purpose = HashPurpose.SERVER_AUTHORIZATION,
|
||||||
|
)
|
||||||
|
.fold(
|
||||||
|
onFailure = { return ResetPasswordResult.Error },
|
||||||
|
onSuccess = { it },
|
||||||
|
)
|
||||||
|
return authSdkSource
|
||||||
|
.makeRegisterKeys(
|
||||||
|
email = activeAccount.profile.email,
|
||||||
|
password = newPassword,
|
||||||
|
kdf = activeAccount.profile.toSdkParams(),
|
||||||
|
)
|
||||||
|
.flatMap { registerKeyResponse ->
|
||||||
|
accountsService.resetPassword(
|
||||||
|
body = ResetPasswordRequestJson(
|
||||||
|
currentPasswordHash = currentPasswordHash,
|
||||||
|
newPasswordHash = registerKeyResponse.masterPasswordHash,
|
||||||
|
passwordHint = passwordHint,
|
||||||
|
key = registerKeyResponse.encryptedUserKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.fold(
|
||||||
|
onSuccess = {
|
||||||
|
// Clear the password reset reason, since it's no longer relevant.
|
||||||
|
storeUserResetPasswordReason(
|
||||||
|
userId = activeAccount.profile.userId,
|
||||||
|
reason = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Update the saved master password hash.
|
||||||
|
authSdkSource
|
||||||
|
.hashPassword(
|
||||||
|
email = activeAccount.profile.email,
|
||||||
|
password = newPassword,
|
||||||
|
kdf = activeAccount.profile.toSdkParams(),
|
||||||
|
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||||
|
)
|
||||||
|
.onSuccess { passwordHash ->
|
||||||
|
authDiskSource.storeMasterPasswordHash(
|
||||||
|
userId = activeAccount.profile.userId,
|
||||||
|
passwordHash = passwordHash,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the login flow.
|
||||||
|
vaultRepository.completeUnlock(userId = activeAccount.profile.userId)
|
||||||
|
|
||||||
|
// Return the success.
|
||||||
|
ResetPasswordResult.Success
|
||||||
|
},
|
||||||
|
onFailure = { ResetPasswordResult.Error },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
override fun setCaptchaCallbackTokenResult(tokenResult: CaptchaCallbackTokenResult) {
|
||||||
mutableCaptchaTokenFlow.tryEmit(tokenResult)
|
mutableCaptchaTokenFlow.tryEmit(tokenResult)
|
||||||
}
|
}
|
||||||
|
@ -854,7 +939,13 @@ class AuthRepositoryImpl(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
||||||
override suspend fun validatePasswordAgainstPolicy(
|
override suspend fun validatePasswordAgainstPolicies(
|
||||||
|
password: String,
|
||||||
|
): Boolean = passwordPolicies
|
||||||
|
.all { validatePasswordAgainstPolicy(password, it) }
|
||||||
|
|
||||||
|
@Suppress("CyclomaticComplexMethod", "ReturnCount")
|
||||||
|
private suspend fun validatePasswordAgainstPolicy(
|
||||||
password: String,
|
password: String,
|
||||||
policy: PolicyInformation.MasterPassword,
|
policy: PolicyInformation.MasterPassword,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
@ -908,10 +999,9 @@ class AuthRepositoryImpl(
|
||||||
.filter { it.enforceOnLogin == true }
|
.filter { it.enforceOnLogin == true }
|
||||||
|
|
||||||
// Check the password against all the policies.
|
// Check the password against all the policies.
|
||||||
val failingPolicies = passwordPolicies.filter { policy ->
|
return passwordPolicies.all { policy ->
|
||||||
!validatePasswordAgainstPolicy(password, policy)
|
validatePasswordAgainstPolicy(password, policy)
|
||||||
}
|
}
|
||||||
return failingPolicies.isEmpty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFingerprintPhrase(
|
private suspend fun getFingerprintPhrase(
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.x8bit.bitwarden.data.auth.repository.model
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models result of resetting a user's password.
|
||||||
|
*/
|
||||||
|
sealed class ResetPasswordResult {
|
||||||
|
/**
|
||||||
|
* The password was reset successfully.
|
||||||
|
*/
|
||||||
|
data object Success : ResetPasswordResult()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There was an error resetting the password.
|
||||||
|
*/
|
||||||
|
data object Error : ResetPasswordResult()
|
||||||
|
}
|
|
@ -42,6 +42,7 @@ data class UserState(
|
||||||
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
|
* @property isVaultUnlocked Whether or not the user's vault is currently unlocked.
|
||||||
* @property isVaultPendingUnlock Whether or not the user's vault is currently pending being
|
* @property isVaultPendingUnlock Whether or not the user's vault is currently pending being
|
||||||
* unlocked, such as when the password policy has not completed verification yet.
|
* unlocked, such as when the password policy has not completed verification yet.
|
||||||
|
* @property needsPasswordReset If the user needs to reset their password.
|
||||||
* @property organizations List of [Organization]s the user is associated with, if any.
|
* @property organizations List of [Organization]s the user is associated with, if any.
|
||||||
* @property isBiometricsEnabled Indicates that the biometrics mechanism for unlocking the
|
* @property isBiometricsEnabled Indicates that the biometrics mechanism for unlocking the
|
||||||
* user's vault is enabled.
|
* user's vault is enabled.
|
||||||
|
@ -57,6 +58,7 @@ data class UserState(
|
||||||
val isLoggedIn: Boolean,
|
val isLoggedIn: Boolean,
|
||||||
val isVaultUnlocked: Boolean,
|
val isVaultUnlocked: Boolean,
|
||||||
val isVaultPendingUnlock: Boolean,
|
val isVaultPendingUnlock: Boolean,
|
||||||
|
val needsPasswordReset: Boolean,
|
||||||
val organizations: List<Organization>,
|
val organizations: List<Organization>,
|
||||||
val isBiometricsEnabled: Boolean,
|
val isBiometricsEnabled: Boolean,
|
||||||
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
val vaultUnlockType: VaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||||
|
|
|
@ -73,8 +73,8 @@ fun UserStateJson.toUserState(
|
||||||
isVaultUnlocked = vaultState.statusFor(userId) ==
|
isVaultUnlocked = vaultState.statusFor(userId) ==
|
||||||
VaultUnlockData.Status.UNLOCKED,
|
VaultUnlockData.Status.UNLOCKED,
|
||||||
isVaultPendingUnlock = vaultState.statusFor(userId) ==
|
isVaultPendingUnlock = vaultState.statusFor(userId) ==
|
||||||
VaultUnlockData.Status.PENDING ||
|
VaultUnlockData.Status.PENDING,
|
||||||
accountJson.profile.forcePasswordResetReason != null,
|
needsPasswordReset = accountJson.profile.forcePasswordResetReason != null,
|
||||||
organizations = userOrganizationsList
|
organizations = userOrganizationsList
|
||||||
.find { it.userId == userId }
|
.find { it.userId == userId }
|
||||||
?.organizations
|
?.organizations
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetpassword
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
|
||||||
|
const val RESET_PASSWORD_ROUTE: String = "reset_password"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the Reset Password screen to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.resetPasswordDestination() {
|
||||||
|
composableWithSlideTransitions(
|
||||||
|
route = RESET_PASSWORD_ROUTE,
|
||||||
|
) {
|
||||||
|
ResetPasswordScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the Reset Password screen.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToResetPasswordGraph(navOptions: NavOptions? = null) {
|
||||||
|
this.navigate(RESET_PASSWORD_ROUTE, navOptions)
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetpassword
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
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.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The top level composable for the Reset Password screen.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
fun ResetPasswordScreen(
|
||||||
|
viewModel: ResetPasswordViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
when (val dialog = state.dialogState) {
|
||||||
|
is ResetPasswordState.DialogState.Error -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = dialog.title ?: R.string.an_error_has_occurred.asText(),
|
||||||
|
message = dialog.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.DialogDismiss) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ResetPasswordState.DialogState.Loading -> {
|
||||||
|
BitwardenLoadingDialog(
|
||||||
|
visibilityState = LoadingDialogState.Shown(
|
||||||
|
text = dialog.message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldShowLogoutConfirmationDialog by remember { mutableStateOf(false) }
|
||||||
|
val onLogoutClicked = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.ConfirmLogoutClick) }
|
||||||
|
}
|
||||||
|
if (shouldShowLogoutConfirmationDialog) {
|
||||||
|
BitwardenTwoButtonDialog(
|
||||||
|
title = stringResource(id = R.string.log_out),
|
||||||
|
message = stringResource(id = R.string.logout_confirmation),
|
||||||
|
confirmButtonText = stringResource(id = R.string.yes),
|
||||||
|
dismissButtonText = stringResource(id = R.string.cancel),
|
||||||
|
onConfirmClick = {
|
||||||
|
shouldShowLogoutConfirmationDialog = false
|
||||||
|
onLogoutClicked()
|
||||||
|
},
|
||||||
|
onDismissClick = { shouldShowLogoutConfirmationDialog = false },
|
||||||
|
onDismissRequest = { shouldShowLogoutConfirmationDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
BitwardenMediumTopAppBar(
|
||||||
|
title = stringResource(id = R.string.update_master_password),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
actions = {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.log_out),
|
||||||
|
onClick = { shouldShowLogoutConfirmationDialog = true },
|
||||||
|
)
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.submit),
|
||||||
|
onClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.SubmitClick) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
ResetPasswordScreeContent(
|
||||||
|
state = state,
|
||||||
|
onCurrentPasswordInputChanged = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(it)) }
|
||||||
|
},
|
||||||
|
onPasswordInputChanged = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(it)) }
|
||||||
|
},
|
||||||
|
onRetypePasswordInputChanged = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(it)) }
|
||||||
|
},
|
||||||
|
onPasswordHintInputChanged = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged(it)) }
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
private fun ResetPasswordScreeContent(
|
||||||
|
state: ResetPasswordState,
|
||||||
|
onCurrentPasswordInputChanged: (String) -> Unit,
|
||||||
|
onPasswordInputChanged: (String) -> Unit,
|
||||||
|
onRetypePasswordInputChanged: (String) -> Unit,
|
||||||
|
onPasswordHintInputChanged: (String) -> Unit,
|
||||||
|
modifier: Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
.imePadding()
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.update_weak_master_password_warning),
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
val passwordPolicyContent = listOf(
|
||||||
|
stringResource(id = R.string.master_password_policy_in_effect),
|
||||||
|
)
|
||||||
|
.plus(state.policies.map { it() })
|
||||||
|
.joinToString("\n • ")
|
||||||
|
Text(
|
||||||
|
text = passwordPolicyContent,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.current_master_password),
|
||||||
|
value = state.currentPasswordInput,
|
||||||
|
onValueChange = onCurrentPasswordInputChanged,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.master_password),
|
||||||
|
value = state.passwordInput,
|
||||||
|
onValueChange = onPasswordInputChanged,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
BitwardenPasswordField(
|
||||||
|
label = stringResource(id = R.string.retype_master_password),
|
||||||
|
value = state.retypePasswordInput,
|
||||||
|
onValueChange = onRetypePasswordInputChanged,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
BitwardenTextField(
|
||||||
|
label = stringResource(id = R.string.master_password_hint),
|
||||||
|
value = state.passwordHintInput,
|
||||||
|
onValueChange = onPasswordHintInputChanged,
|
||||||
|
hint = stringResource(id = R.string.master_password_hint_description),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,409 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetpassword
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
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.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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
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"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages application state for the Reset Password screen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
|
class ResetPasswordViewModel @Inject constructor(
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<ResetPasswordState, ResetPasswordEvent, ResetPasswordAction>(
|
||||||
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
?: ResetPasswordState(
|
||||||
|
policies = authRepository.passwordPolicies.toDisplayLabels(),
|
||||||
|
dialogState = null,
|
||||||
|
currentPasswordInput = "",
|
||||||
|
passwordInput = "",
|
||||||
|
retypePasswordInput = "",
|
||||||
|
passwordHintInput = "",
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
init {
|
||||||
|
// As state updates, write to saved state handle.
|
||||||
|
stateFlow
|
||||||
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: ResetPasswordAction) {
|
||||||
|
when (action) {
|
||||||
|
ResetPasswordAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
||||||
|
ResetPasswordAction.SubmitClick -> handleSubmitClicked()
|
||||||
|
ResetPasswordAction.DialogDismiss -> handleDialogDismiss()
|
||||||
|
|
||||||
|
is ResetPasswordAction.CurrentPasswordInputChanged -> {
|
||||||
|
handleCurrentPasswordInputChanged(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ResetPasswordAction.PasswordInputChanged -> handlePasswordInputChanged(action)
|
||||||
|
|
||||||
|
is ResetPasswordAction.RetypePasswordInputChanged -> {
|
||||||
|
handleRetypePasswordInputChanged(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ResetPasswordAction.PasswordHintInputChanged -> {
|
||||||
|
handlePasswordHintInputChanged(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ResetPasswordAction.Internal.ReceiveResetPasswordResult -> {
|
||||||
|
handleReceiveResetPasswordResult(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ResetPasswordAction.Internal.ReceiveValidatePasswordAgainstPoliciesResult -> {
|
||||||
|
handleReceiveValidatePasswordAgainstPoliciesResult(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ResetPasswordAction.Internal.ReceiveValidatePasswordResult -> {
|
||||||
|
handleReceiveValidatePasswordResult(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the view if the user confirms logging out.
|
||||||
|
*/
|
||||||
|
private fun handleConfirmLogoutClick() {
|
||||||
|
authRepository.logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the user's current password when they submit.
|
||||||
|
*/
|
||||||
|
private fun handleSubmitClicked() {
|
||||||
|
// Display an error dialog if the new password field is blank.
|
||||||
|
val password = state.passwordInput
|
||||||
|
if (password.isBlank()) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.validation_field_required
|
||||||
|
.asText(R.string.master_password.asText()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the new password meets the policy requirements.
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = authRepository.validatePasswordAgainstPolicies(password)
|
||||||
|
sendAction(
|
||||||
|
ResetPasswordAction.Internal.ReceiveValidatePasswordAgainstPoliciesResult(result),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss the dialog state.
|
||||||
|
*/
|
||||||
|
private fun handleDialogDismiss() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state with the current password input.
|
||||||
|
*/
|
||||||
|
private fun handleCurrentPasswordInputChanged(
|
||||||
|
action: ResetPasswordAction.CurrentPasswordInputChanged,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
currentPasswordInput = action.input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state with the new password input.
|
||||||
|
*/
|
||||||
|
private fun handlePasswordInputChanged(action: ResetPasswordAction.PasswordInputChanged) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
passwordInput = action.input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state with the re-typed password input.
|
||||||
|
*/
|
||||||
|
private fun handleRetypePasswordInputChanged(
|
||||||
|
action: ResetPasswordAction.RetypePasswordInputChanged,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
retypePasswordInput = action.input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the state with the password hint input.
|
||||||
|
*/
|
||||||
|
private fun handlePasswordHintInputChanged(
|
||||||
|
action: ResetPasswordAction.PasswordHintInputChanged,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
passwordHintInput = action.input,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show an alert if the reset password attempt failed.
|
||||||
|
*/
|
||||||
|
private fun handleReceiveResetPasswordResult(
|
||||||
|
action: ResetPasswordAction.Internal.ReceiveResetPasswordResult,
|
||||||
|
) {
|
||||||
|
// End the loading state.
|
||||||
|
mutableStateFlow.update { it.copy(dialogState = null) }
|
||||||
|
|
||||||
|
when (action.result) {
|
||||||
|
// Display an alert if there was an error.
|
||||||
|
ResetPasswordResult.Error -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO-OP: The root nav view model will handle the completed auth flow.
|
||||||
|
ResetPasswordResult.Success -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an error if the current password is valid or if there was an error, and
|
||||||
|
* otherwise, reset the master password.
|
||||||
|
*/
|
||||||
|
private fun handleReceiveValidatePasswordResult(
|
||||||
|
action: ResetPasswordAction.Internal.ReceiveValidatePasswordResult,
|
||||||
|
) {
|
||||||
|
when (action.result) {
|
||||||
|
// Display an alert if there was an error.
|
||||||
|
ValidatePasswordResult.Error -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ValidatePasswordResult.Success -> {
|
||||||
|
// Display an error dialog if the password is invalid.
|
||||||
|
if (!action.result.isValid) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.invalid_master_password.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show the loading dialog.
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Loading(
|
||||||
|
message = R.string.updating_password.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = authRepository.resetPassword(
|
||||||
|
currentPassword = state.currentPasswordInput,
|
||||||
|
newPassword = state.passwordInput,
|
||||||
|
passwordHint = state.passwordHintInput,
|
||||||
|
)
|
||||||
|
trySendAction(
|
||||||
|
ResetPasswordAction.Internal.ReceiveResetPasswordResult(result),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display an alert if the password doesn't meet the policy requirements, then check that
|
||||||
|
* the new password matches the retyped password and that the current password is valid.
|
||||||
|
*/
|
||||||
|
private fun handleReceiveValidatePasswordAgainstPoliciesResult(
|
||||||
|
action: ResetPasswordAction.Internal.ReceiveValidatePasswordAgainstPoliciesResult,
|
||||||
|
) {
|
||||||
|
// Display an error alert if the new password doesn't meet the policy requirements.
|
||||||
|
if (!action.meetsRequirements) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = R.string.master_password_policy_validation_title.asText(),
|
||||||
|
message = R.string.master_password_policy_validation_message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display an error alert if the re-typed password doesn't match the new password.
|
||||||
|
if (state.passwordInput != state.retypePasswordInput) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.master_password_confirmation_val_message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the entered current password is correct.
|
||||||
|
viewModelScope.launch {
|
||||||
|
val currentPassword = state.currentPasswordInput
|
||||||
|
val result = authRepository.validatePassword(currentPassword)
|
||||||
|
trySendAction(ResetPasswordAction.Internal.ReceiveValidatePasswordResult(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models state of the Reset Password screen.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class ResetPasswordState(
|
||||||
|
val policies: List<Text>,
|
||||||
|
val dialogState: DialogState?,
|
||||||
|
val currentPasswordInput: String,
|
||||||
|
val passwordInput: String,
|
||||||
|
val retypePasswordInput: String,
|
||||||
|
val passwordHintInput: String,
|
||||||
|
) : Parcelable {
|
||||||
|
/**
|
||||||
|
* Represents the current state of any dialogs on the screen.
|
||||||
|
*/
|
||||||
|
sealed class DialogState : Parcelable {
|
||||||
|
/**
|
||||||
|
* Represents an error dialog with the given [message] and optional [title]. If no title
|
||||||
|
* is specified a default will be provided.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val title: Text? = null,
|
||||||
|
val message: Text,
|
||||||
|
) : DialogState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a loading dialog with the given [message].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Loading(
|
||||||
|
val message: Text,
|
||||||
|
) : DialogState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models events for the Reset Password screen.
|
||||||
|
*/
|
||||||
|
sealed class ResetPasswordEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions for the Reset Password screen.
|
||||||
|
*/
|
||||||
|
sealed class ResetPasswordAction {
|
||||||
|
/**
|
||||||
|
* Indicates that the user has confirmed logging out.
|
||||||
|
*/
|
||||||
|
data object ConfirmLogoutClick : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the user has clicked the submit button.
|
||||||
|
*/
|
||||||
|
data object SubmitClick : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the dialog has been dismissed.
|
||||||
|
*/
|
||||||
|
data object DialogDismiss : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the current password input has changed.
|
||||||
|
*/
|
||||||
|
data class CurrentPasswordInputChanged(val input: String) : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the new password input has changed.
|
||||||
|
*/
|
||||||
|
data class PasswordInputChanged(val input: String) : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the re-type password input has changed.
|
||||||
|
*/
|
||||||
|
data class RetypePasswordInputChanged(val input: String) : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that the password hint input has changed.
|
||||||
|
*/
|
||||||
|
data class PasswordHintInputChanged(val input: String) : ResetPasswordAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions that the [ResetPasswordViewModel] might send itself.
|
||||||
|
*/
|
||||||
|
sealed class Internal : ResetPasswordAction() {
|
||||||
|
/**
|
||||||
|
* Indicates that a reset password result has been received.
|
||||||
|
*/
|
||||||
|
data class ReceiveResetPasswordResult(
|
||||||
|
val result: ResetPasswordResult,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a validate password result has been received.
|
||||||
|
*/
|
||||||
|
data class ReceiveValidatePasswordResult(
|
||||||
|
val result: ValidatePasswordResult,
|
||||||
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a validate password against policies result has been received.
|
||||||
|
*/
|
||||||
|
data class ReceiveValidatePasswordAgainstPoliciesResult(
|
||||||
|
val meetsRequirements: Boolean,
|
||||||
|
) : Internal()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetpassword.util
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.PolicyInformation
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a list of master password policies into a list of text instructions
|
||||||
|
* for the user about what requirements the password must meet.
|
||||||
|
*/
|
||||||
|
fun List<PolicyInformation.MasterPassword>.toDisplayLabels(): List<Text> {
|
||||||
|
val list = mutableListOf<Text>()
|
||||||
|
|
||||||
|
mapNotNull { it.minLength }.maxOrNull()?.let {
|
||||||
|
list.add(R.string.policy_in_effect_min_length.asText(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
mapNotNull { it.minComplexity }.maxOrNull()?.let {
|
||||||
|
list.add(R.string.policy_in_effect_min_complexity.asText(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapNotNull { it.requireUpper }.any { it }) {
|
||||||
|
list.add(R.string.policy_in_effect_uppercase.asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapNotNull { it.requireLower }.any { it }) {
|
||||||
|
list.add(R.string.policy_in_effect_lowercase.asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapNotNull { it.requireNumbers }.any { it }) {
|
||||||
|
list.add(R.string.policy_in_effect_numbers.asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapNotNull { it.requireSpecial }.any { it }) {
|
||||||
|
list.add(R.string.policy_in_effect_special.asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
|
@ -14,6 +14,9 @@ import androidx.navigation.navOptions
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE
|
import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
|
import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph
|
||||||
|
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.resetPasswordDestination
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.VAULT_UNLOCK_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.navigateToVaultUnlock
|
||||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
|
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.vaultUnlockDestination
|
||||||
|
@ -24,6 +27,8 @@ import com.x8bit.bitwarden.ui.platform.feature.splash.splashDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAPH_ROUTE
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.VAULT_UNLOCKED_GRAPH_ROUTE
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.navigateToVaultUnlockedGraph
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph
|
import com.x8bit.bitwarden.ui.platform.feature.vaultunlocked.vaultUnlockedGraph
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.NonNullEnterTransitionProvider
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.NonNullExitTransitionProvider
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
|
import com.x8bit.bitwarden.ui.platform.theme.RootTransitionProviders
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
|
||||||
|
@ -35,7 +40,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||||
/**
|
/**
|
||||||
* Controls root level [NavHost] for the app.
|
* Controls root level [NavHost] for the app.
|
||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun RootNavScreen(
|
fun RootNavScreen(
|
||||||
viewModel: RootNavViewModel = hiltViewModel(),
|
viewModel: RootNavViewModel = hiltViewModel(),
|
||||||
|
@ -62,19 +67,21 @@ fun RootNavScreen(
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = SPLASH_ROUTE,
|
startDestination = SPLASH_ROUTE,
|
||||||
enterTransition = RootTransitionProviders.Enter.fadeIn,
|
enterTransition = { this.targetState.destination.route.toEnterTransition()(this) },
|
||||||
exitTransition = RootTransitionProviders.Exit.fadeOut,
|
exitTransition = { this.targetState.destination.route.toExitTransition()(this) },
|
||||||
popEnterTransition = RootTransitionProviders.Enter.fadeIn,
|
popEnterTransition = { this.targetState.destination.route.toEnterTransition()(this) },
|
||||||
popExitTransition = RootTransitionProviders.Exit.fadeOut,
|
popExitTransition = { this.targetState.destination.route.toExitTransition()(this) },
|
||||||
) {
|
) {
|
||||||
splashDestination()
|
splashDestination()
|
||||||
authGraph(navController)
|
authGraph(navController)
|
||||||
|
resetPasswordDestination()
|
||||||
vaultUnlockDestination()
|
vaultUnlockDestination()
|
||||||
vaultUnlockedGraph(navController)
|
vaultUnlockedGraph(navController)
|
||||||
}
|
}
|
||||||
|
|
||||||
val targetRoute = when (state) {
|
val targetRoute = when (state) {
|
||||||
RootNavState.Auth -> AUTH_GRAPH_ROUTE
|
RootNavState.Auth -> AUTH_GRAPH_ROUTE
|
||||||
|
RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE
|
||||||
RootNavState.Splash -> SPLASH_ROUTE
|
RootNavState.Splash -> SPLASH_ROUTE
|
||||||
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
RootNavState.VaultLocked -> VAULT_UNLOCK_ROUTE
|
||||||
is RootNavState.VaultUnlocked,
|
is RootNavState.VaultUnlocked,
|
||||||
|
@ -107,6 +114,7 @@ fun RootNavScreen(
|
||||||
|
|
||||||
when (val currentState = state) {
|
when (val currentState = state) {
|
||||||
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
RootNavState.Auth -> navController.navigateToAuthGraph(rootNavOptions)
|
||||||
|
RootNavState.ResetPassword -> navController.navigateToResetPasswordGraph(rootNavOptions)
|
||||||
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
RootNavState.Splash -> navController.navigateToSplash(rootNavOptions)
|
||||||
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
RootNavState.VaultLocked -> navController.navigateToVaultUnlock(rootNavOptions)
|
||||||
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
is RootNavState.VaultUnlocked -> navController.navigateToVaultUnlockedGraph(rootNavOptions)
|
||||||
|
@ -144,3 +152,19 @@ private fun NavDestination?.rootLevelRoute(): String? {
|
||||||
}
|
}
|
||||||
return parent.rootLevelRoute()
|
return parent.rootLevelRoute()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the enter transition for each route.
|
||||||
|
*/
|
||||||
|
private fun String?.toEnterTransition(): NonNullEnterTransitionProvider = when (this) {
|
||||||
|
RESET_PASSWORD_ROUTE -> RootTransitionProviders.Enter.slideUp
|
||||||
|
else -> RootTransitionProviders.Enter.fadeIn
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the exit transition for each route.
|
||||||
|
*/
|
||||||
|
private fun String?.toExitTransition(): NonNullExitTransitionProvider = when (this) {
|
||||||
|
RESET_PASSWORD_ROUTE -> RootTransitionProviders.Exit.slideDown
|
||||||
|
else -> RootTransitionProviders.Exit.fadeOut
|
||||||
|
}
|
||||||
|
|
|
@ -59,6 +59,8 @@ class RootNavViewModel @Inject constructor(
|
||||||
val userState = action.userState
|
val userState = action.userState
|
||||||
val specialCircumstance = action.specialCircumstance
|
val specialCircumstance = action.specialCircumstance
|
||||||
val updatedRootNavState = when {
|
val updatedRootNavState = when {
|
||||||
|
userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword
|
||||||
|
|
||||||
userState == null ||
|
userState == null ||
|
||||||
!userState.activeAccount.isLoggedIn ||
|
!userState.activeAccount.isLoggedIn ||
|
||||||
userState.activeAccount.isVaultPendingUnlock ||
|
userState.activeAccount.isVaultPendingUnlock ||
|
||||||
|
@ -99,6 +101,12 @@ sealed class RootNavState : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Auth : RootNavState()
|
data object Auth : RootNavState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App should show reset password graph.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object ResetPassword : RootNavState()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* App should show splash nav graph.
|
* App should show splash nav graph.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,7 +21,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
@ -71,7 +70,7 @@ fun ExportVaultScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldShowConfirmationDialog by remember { mutableStateOf(false) }
|
var shouldShowConfirmationDialog by remember { mutableStateOf(false) }
|
||||||
var confirmExportVaultClicked = remember(viewModel) {
|
val confirmExportVaultClicked = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) }
|
{ viewModel.trySendAction(ExportVaultAction.ConfirmExportVaultClicked) }
|
||||||
}
|
}
|
||||||
if (shouldShowConfirmationDialog) {
|
if (shouldShowConfirmationDialog) {
|
||||||
|
@ -154,7 +153,6 @@ fun ExportVaultScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExportVaultScreenContent(
|
private fun ExportVaultScreenContent(
|
||||||
state: ExportVaultState,
|
state: ExportVaultState,
|
||||||
|
|
|
@ -263,6 +263,7 @@ class MainViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,7 +7,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintRespon
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
@ -217,7 +218,7 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||||
val response = MockResponse().setBody("")
|
val response = MockResponse().setBody("")
|
||||||
server.enqueue(response)
|
server.enqueue(response)
|
||||||
val result = service.resendVerificationCodeEmail(
|
val result = service.resendVerificationCodeEmail(
|
||||||
body = ResendEmailJsonRequest(
|
body = ResendEmailRequestJson(
|
||||||
deviceIdentifier = "3",
|
deviceIdentifier = "3",
|
||||||
email = "example@email.com",
|
email = "example@email.com",
|
||||||
passwordHash = "37y4d8r379r4789nt387r39k3dr87nr93",
|
passwordHash = "37y4d8r379r4789nt387r39k3dr87nr93",
|
||||||
|
@ -227,6 +228,21 @@ class AccountsServiceTest : BaseServiceTest() {
|
||||||
assertTrue(result.isSuccess)
|
assertTrue(result.isSuccess)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resetPassword with empty response is success`() = runTest {
|
||||||
|
val response = MockResponse().setBody("")
|
||||||
|
server.enqueue(response)
|
||||||
|
val result = service.resetPassword(
|
||||||
|
body = ResetPasswordRequestJson(
|
||||||
|
currentPasswordHash = "",
|
||||||
|
newPasswordHash = "",
|
||||||
|
passwordHint = null,
|
||||||
|
key = "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertTrue(result.isSuccess)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EMAIL = "email"
|
private const val EMAIL = "email"
|
||||||
private val registerRequestBody = RegisterRequestJson(
|
private val registerRequestBody = RegisterRequestJson(
|
||||||
|
|
|
@ -22,7 +22,8 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResp
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailJsonRequest
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResendEmailRequestJson
|
||||||
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.ResetPasswordRequestJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorAuthMethod
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||||
|
@ -53,11 +54,11 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
import com.x8bit.bitwarden.data.auth.repository.model.PrevalidateSsoResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ResendEmailResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.ResetPasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
import com.x8bit.bitwarden.data.auth.repository.model.UserOrganizations
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.createMockMasterPasswordPolicy
|
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
import com.x8bit.bitwarden.data.auth.repository.util.SsoCallbackResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
import com.x8bit.bitwarden.data.auth.repository.util.toOrganizations
|
||||||
|
@ -1940,6 +1941,155 @@ class AuthRepositoryTest {
|
||||||
assertEquals(RegisterResult.Error(errorMessage = "message"), result)
|
assertEquals(RegisterResult.Error(errorMessage = "message"), result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resetPassword Success should return Success`() = runTest {
|
||||||
|
val currentPassword = "currentPassword"
|
||||||
|
val currentPasswordHash = "hashedCurrentPassword"
|
||||||
|
val password = "password"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
coEvery {
|
||||||
|
authSdkSource.hashPassword(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = currentPassword,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
purpose = HashPurpose.SERVER_AUTHORIZATION,
|
||||||
|
)
|
||||||
|
} returns currentPasswordHash.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
authSdkSource.makeRegisterKeys(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = password,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
)
|
||||||
|
} returns Result.success(
|
||||||
|
RegisterKeyResponse(
|
||||||
|
masterPasswordHash = PASSWORD_HASH,
|
||||||
|
encryptedUserKey = ENCRYPTED_USER_KEY,
|
||||||
|
keys = RsaKeyPair(
|
||||||
|
public = PUBLIC_KEY,
|
||||||
|
private = PRIVATE_KEY,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
accountsService.resetPassword(
|
||||||
|
body = ResetPasswordRequestJson(
|
||||||
|
currentPasswordHash = currentPasswordHash,
|
||||||
|
newPasswordHash = PASSWORD_HASH,
|
||||||
|
passwordHint = null,
|
||||||
|
key = ENCRYPTED_USER_KEY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} returns Unit.asSuccess()
|
||||||
|
|
||||||
|
val result = repository.resetPassword(
|
||||||
|
currentPassword = currentPassword,
|
||||||
|
newPassword = password,
|
||||||
|
passwordHint = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ResetPasswordResult.Success,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authSdkSource.makeRegisterKeys(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = password,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
)
|
||||||
|
accountsService.resetPassword(
|
||||||
|
body = ResetPasswordRequestJson(
|
||||||
|
currentPasswordHash = currentPasswordHash,
|
||||||
|
newPasswordHash = PASSWORD_HASH,
|
||||||
|
passwordHint = null,
|
||||||
|
key = ENCRYPTED_USER_KEY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify {
|
||||||
|
vaultRepository.completeUnlock(userId = USER_ID_1)
|
||||||
|
}
|
||||||
|
fakeAuthDiskSource.assertMasterPasswordHash(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
passwordHash = PASSWORD_HASH,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `resetPassword Failure should return Error`() = runTest {
|
||||||
|
val currentPassword = "currentPassword"
|
||||||
|
val currentPasswordHash = "hashedCurrentPassword"
|
||||||
|
val password = "password"
|
||||||
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
|
coEvery {
|
||||||
|
authSdkSource.hashPassword(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = currentPassword,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
purpose = HashPurpose.SERVER_AUTHORIZATION,
|
||||||
|
)
|
||||||
|
} returns currentPasswordHash.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
authSdkSource.makeRegisterKeys(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = password,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
)
|
||||||
|
} returns Result.success(
|
||||||
|
RegisterKeyResponse(
|
||||||
|
masterPasswordHash = PASSWORD_HASH,
|
||||||
|
encryptedUserKey = ENCRYPTED_USER_KEY,
|
||||||
|
keys = RsaKeyPair(
|
||||||
|
public = PUBLIC_KEY,
|
||||||
|
private = PRIVATE_KEY,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
accountsService.resetPassword(
|
||||||
|
body = ResetPasswordRequestJson(
|
||||||
|
currentPasswordHash = currentPasswordHash,
|
||||||
|
newPasswordHash = PASSWORD_HASH,
|
||||||
|
passwordHint = null,
|
||||||
|
key = ENCRYPTED_USER_KEY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} returns Throwable("Fail").asFailure()
|
||||||
|
|
||||||
|
val result = repository.resetPassword(
|
||||||
|
currentPassword = currentPassword,
|
||||||
|
newPassword = password,
|
||||||
|
passwordHint = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
ResetPasswordResult.Error,
|
||||||
|
result,
|
||||||
|
)
|
||||||
|
coVerify {
|
||||||
|
authSdkSource.hashPassword(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = currentPassword,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
purpose = HashPurpose.SERVER_AUTHORIZATION,
|
||||||
|
)
|
||||||
|
authSdkSource.makeRegisterKeys(
|
||||||
|
email = ACCOUNT_1.profile.email,
|
||||||
|
password = password,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
)
|
||||||
|
accountsService.resetPassword(
|
||||||
|
body = ResetPasswordRequestJson(
|
||||||
|
currentPasswordHash = currentPasswordHash,
|
||||||
|
newPasswordHash = PASSWORD_HASH,
|
||||||
|
passwordHint = null,
|
||||||
|
key = ENCRYPTED_USER_KEY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `passwordHintRequest with valid email should return Success`() = runTest {
|
fun `passwordHintRequest with valid email should return Success`() = runTest {
|
||||||
val email = "valid@example.com"
|
val email = "valid@example.com"
|
||||||
|
@ -2129,7 +2279,7 @@ class AuthRepositoryTest {
|
||||||
// Resend the verification code email.
|
// Resend the verification code email.
|
||||||
coEvery {
|
coEvery {
|
||||||
accountsService.resendVerificationCodeEmail(
|
accountsService.resendVerificationCodeEmail(
|
||||||
body = ResendEmailJsonRequest(
|
body = ResendEmailRequestJson(
|
||||||
deviceIdentifier = UNIQUE_APP_ID,
|
deviceIdentifier = UNIQUE_APP_ID,
|
||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
passwordHash = PASSWORD_HASH,
|
passwordHash = PASSWORD_HASH,
|
||||||
|
@ -2141,7 +2291,7 @@ class AuthRepositoryTest {
|
||||||
assertEquals(ResendEmailResult.Success, resendEmailResult)
|
assertEquals(ResendEmailResult.Success, resendEmailResult)
|
||||||
coVerify {
|
coVerify {
|
||||||
accountsService.resendVerificationCodeEmail(
|
accountsService.resendVerificationCodeEmail(
|
||||||
body = ResendEmailJsonRequest(
|
body = ResendEmailRequestJson(
|
||||||
deviceIdentifier = UNIQUE_APP_ID,
|
deviceIdentifier = UNIQUE_APP_ID,
|
||||||
email = EMAIL,
|
email = EMAIL,
|
||||||
passwordHash = PASSWORD_HASH,
|
passwordHash = PASSWORD_HASH,
|
||||||
|
@ -2944,34 +3094,61 @@ class AuthRepositoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `validatePasswordAgainstPolicy validates password against policy requirements`() = runTest {
|
fun `validatePasswordAgainstPolicy validates password against policy requirements`() = runTest {
|
||||||
var policy = createMockMasterPasswordPolicy(minLength = 10)
|
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
||||||
assertFalse(repository.validatePasswordAgainstPolicy(password = "123", policy = policy))
|
|
||||||
|
// A helper method to set a policy in the store with the given parameters.
|
||||||
|
fun setPolicy(
|
||||||
|
minLength: Int = 0,
|
||||||
|
minComplexity: Int? = null,
|
||||||
|
requireUpper: Boolean = false,
|
||||||
|
requireLower: Boolean = false,
|
||||||
|
requireNumbers: Boolean = false,
|
||||||
|
requireSpecial: Boolean = false,
|
||||||
|
) {
|
||||||
|
fakeAuthDiskSource.storePolicies(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
policies = listOf(
|
||||||
|
createMockPolicy(
|
||||||
|
type = PolicyTypeJson.MASTER_PASSWORD,
|
||||||
|
isEnabled = true,
|
||||||
|
data = buildJsonObject {
|
||||||
|
put(key = "minLength", value = minLength)
|
||||||
|
put(key = "minComplexity", value = minComplexity)
|
||||||
|
put(key = "requireUpper", value = requireUpper)
|
||||||
|
put(key = "requireLower", value = requireLower)
|
||||||
|
put(key = "requireNumbers", value = requireNumbers)
|
||||||
|
put(key = "requireSpecial", value = requireSpecial)
|
||||||
|
put(key = "enforceOnLogin", value = true)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPolicy(minLength = 10)
|
||||||
|
assertFalse(repository.validatePasswordAgainstPolicies(password = "123"))
|
||||||
|
|
||||||
val password = "simple"
|
val password = "simple"
|
||||||
fakeAuthDiskSource.userState = SINGLE_USER_STATE_1
|
|
||||||
coEvery {
|
coEvery {
|
||||||
authSdkSource.passwordStrength(
|
authSdkSource.passwordStrength(
|
||||||
email = SINGLE_USER_STATE_1.activeAccount.profile.email,
|
email = SINGLE_USER_STATE_1.activeAccount.profile.email,
|
||||||
password = password,
|
password = password,
|
||||||
)
|
)
|
||||||
} returns Result.success(LEVEL_0)
|
} returns Result.success(LEVEL_0)
|
||||||
policy = createMockMasterPasswordPolicy(minComplexity = 1)
|
setPolicy(minComplexity = 10)
|
||||||
assertFalse(repository.validatePasswordAgainstPolicy(password = password, policy = policy))
|
assertFalse(repository.validatePasswordAgainstPolicies(password = password))
|
||||||
|
|
||||||
policy = createMockMasterPasswordPolicy(requireUpper = true)
|
setPolicy(requireUpper = true)
|
||||||
assertFalse(repository.validatePasswordAgainstPolicy(password = "lower", policy = policy))
|
assertFalse(repository.validatePasswordAgainstPolicies(password = "lower"))
|
||||||
|
|
||||||
policy = createMockMasterPasswordPolicy(requireLower = true)
|
setPolicy(requireLower = true)
|
||||||
assertFalse(repository.validatePasswordAgainstPolicy(password = "UPPER", policy = policy))
|
assertFalse(repository.validatePasswordAgainstPolicies(password = "UPPER"))
|
||||||
|
|
||||||
policy = createMockMasterPasswordPolicy(requireNumbers = true)
|
setPolicy(requireNumbers = true)
|
||||||
assertFalse(repository.validatePasswordAgainstPolicy(password = "letters", policy = policy))
|
assertFalse(repository.validatePasswordAgainstPolicies(password = "letters"))
|
||||||
|
|
||||||
policy = createMockMasterPasswordPolicy(requireSpecial = true)
|
setPolicy(requireSpecial = true)
|
||||||
assertFalse(repository.validatePasswordAgainstPolicy(password = "letters", policy = policy))
|
assertFalse(repository.validatePasswordAgainstPolicies(password = "letters"))
|
||||||
|
|
||||||
policy = createMockMasterPasswordPolicy(minLength = 5)
|
|
||||||
assertTrue(repository.validatePasswordAgainstPolicy(password = "password", policy = policy))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -108,6 +108,7 @@ class UserStateJsonExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
id = "organizationId",
|
id = "organizationId",
|
||||||
|
@ -183,6 +184,7 @@ class UserStateJsonExtensionsTest {
|
||||||
isLoggedIn = false,
|
isLoggedIn = false,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
id = "organizationId",
|
id = "organizationId",
|
||||||
|
|
|
@ -72,6 +72,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -204,6 +205,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
)
|
)
|
||||||
|
@ -255,6 +257,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = false,
|
isLoggedIn = false,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -128,6 +128,7 @@ class LoginViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -0,0 +1,180 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetPassword
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assert
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.isDisplayed
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
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
|
||||||
|
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.BaseComposeTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||||
|
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.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ResetPasswordScreenTest : BaseComposeTest() {
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<ResetPasswordEvent>()
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
val viewModel = mockk<ResetPasswordViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
ResetPasswordScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `basicDialog should update according to state`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Error message")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = "Error message".asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Error message")
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
.isDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadingDialog should update according to state`() {
|
||||||
|
composeTestRule.onNodeWithText("Loading...").assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Loading(
|
||||||
|
message = "Loading...".asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Loading...").isDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `logout button click should display confirmation dialog and emit ConfirmLogoutClick`() {
|
||||||
|
composeTestRule.onNodeWithText("Log out").performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Are you sure you want to log out?")
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
.isDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Yes")
|
||||||
|
.assert(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.ConfirmLogoutClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `submit button click should emit SubmitClick`() {
|
||||||
|
composeTestRule.onNodeWithText("Submit").performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `password instructions should update according to state`() {
|
||||||
|
val baseString =
|
||||||
|
"One or more organization policies require your master password to meet the following requirements:"
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(baseString)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
policies = listOf("Make password better".asText()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedString = listOf(
|
||||||
|
baseString,
|
||||||
|
"Make password better",
|
||||||
|
)
|
||||||
|
.joinToString("\n • ")
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(updatedString)
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `current password input change should send CurrentPasswordInputChanged action`() {
|
||||||
|
val input = "Test123"
|
||||||
|
composeTestRule.onNodeWithText("Current master password").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `password input change should send PasswordInputChange action`() {
|
||||||
|
val input = "Test123"
|
||||||
|
composeTestRule.onNodeWithText("Master password").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retype password input change should send RetypePasswordInputChanged action`() {
|
||||||
|
val input = "Test123"
|
||||||
|
composeTestRule.onNodeWithText("Re-type master password").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `password hint input change should send PasswordHintInputChanged action`() {
|
||||||
|
val input = "Test123"
|
||||||
|
composeTestRule.onNodeWithText("Master password hint (optional)").performTextInput(input)
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE = ResetPasswordState(
|
||||||
|
policies = emptyList(),
|
||||||
|
dialogState = null,
|
||||||
|
currentPasswordInput = "",
|
||||||
|
passwordInput = "",
|
||||||
|
retypePasswordInput = "",
|
||||||
|
passwordHintInput = "",
|
||||||
|
)
|
|
@ -0,0 +1,321 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetPassword
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
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.resetpassword.ResetPasswordAction
|
||||||
|
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
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class ResetPasswordViewModelTest : BaseViewModelTest() {
|
||||||
|
private val authRepository: AuthRepository = mockk() {
|
||||||
|
every { passwordPolicies } returns emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val savedStateHandle = SavedStateHandle()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ConfirmLogoutClick logs out`() = runTest {
|
||||||
|
every { authRepository.logout() } just runs
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.ConfirmLogoutClick)
|
||||||
|
|
||||||
|
verify { authRepository.logout() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CurrentPasswordInputChanged should update the current password input in the state`() =
|
||||||
|
runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged("Test123"))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
currentPasswordInput = "Test123",
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubmitClicked with blank password shows error alert`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.validation_field_required
|
||||||
|
.asText(R.string.master_password.asText()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dismiss the alert.
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.DialogDismiss)
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubmitClicked with invalid password shows error alert`() = runTest {
|
||||||
|
val password = "Test123"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePasswordAgainstPolicies(password)
|
||||||
|
} returns false
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = R.string.master_password_policy_validation_title.asText(),
|
||||||
|
message = R.string.master_password_policy_validation_message.asText(),
|
||||||
|
),
|
||||||
|
passwordInput = password,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubmitClicked with non-matching retyped password shows error alert`() = runTest {
|
||||||
|
val password = "Test123"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePasswordAgainstPolicies(password)
|
||||||
|
} returns true
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.master_password_confirmation_val_message.asText(),
|
||||||
|
),
|
||||||
|
passwordInput = password,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubmitClicked with error for validating current password shows error alert`() = runTest {
|
||||||
|
val currentPassword = "CurrentTest123"
|
||||||
|
val password = "Test123"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePasswordAgainstPolicies(password)
|
||||||
|
} returns true
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePassword(currentPassword)
|
||||||
|
} returns ValidatePasswordResult.Error
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.generic_error_message.asText(),
|
||||||
|
),
|
||||||
|
currentPasswordInput = currentPassword,
|
||||||
|
passwordInput = password,
|
||||||
|
retypePasswordInput = password,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubmitClicked with invalid current password shows alert`() = runTest {
|
||||||
|
val currentPassword = "CurrentTest123"
|
||||||
|
val password = "Test123"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePasswordAgainstPolicies(password)
|
||||||
|
} returns true
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePassword(currentPassword)
|
||||||
|
} returns ValidatePasswordResult.Success(isValid = false)
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Error(
|
||||||
|
title = null,
|
||||||
|
message = R.string.invalid_master_password.asText(),
|
||||||
|
),
|
||||||
|
currentPasswordInput = currentPassword,
|
||||||
|
passwordInput = password,
|
||||||
|
retypePasswordInput = password,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SubmitClicked with all valid inputs resets password`() = runTest {
|
||||||
|
val currentPassword = "CurrentTest123"
|
||||||
|
val password = "Test123"
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePasswordAgainstPolicies(password)
|
||||||
|
} returns true
|
||||||
|
coEvery {
|
||||||
|
authRepository.validatePassword(currentPassword)
|
||||||
|
} returns ValidatePasswordResult.Success(isValid = true)
|
||||||
|
coEvery {
|
||||||
|
authRepository.resetPassword(any(), any(), any())
|
||||||
|
} returns ResetPasswordResult.Success
|
||||||
|
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.CurrentPasswordInputChanged(currentPassword))
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged(password))
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged(password))
|
||||||
|
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = null,
|
||||||
|
currentPasswordInput = currentPassword,
|
||||||
|
passwordInput = password,
|
||||||
|
retypePasswordInput = password,
|
||||||
|
),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.SubmitClick)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = ResetPasswordState.DialogState.Loading(
|
||||||
|
message = R.string.updating_password.asText(),
|
||||||
|
),
|
||||||
|
currentPasswordInput = currentPassword,
|
||||||
|
passwordInput = password,
|
||||||
|
retypePasswordInput = password,
|
||||||
|
),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialogState = null,
|
||||||
|
currentPasswordInput = currentPassword,
|
||||||
|
passwordInput = password,
|
||||||
|
retypePasswordInput = password,
|
||||||
|
),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
|
||||||
|
coVerify { authRepository.resetPassword(any(), any(), any()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PasswordInputChanged should update the password input in the state`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordInputChanged("Test123"))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
passwordInput = "Test123",
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `RetypePasswordInputChanged should update the retype password input in the state`() =
|
||||||
|
runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.RetypePasswordInputChanged("Test123"))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
retypePasswordInput = "Test123",
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `PasswordHintInputChanged should update the password hint input in the state`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ResetPasswordAction.PasswordHintInputChanged("Test123"))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
passwordHintInput = "Test123",
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(): ResetPasswordViewModel =
|
||||||
|
ResetPasswordViewModel(
|
||||||
|
authRepository = authRepository,
|
||||||
|
savedStateHandle = savedStateHandle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE = ResetPasswordState(
|
||||||
|
policies = emptyList(),
|
||||||
|
dialogState = null,
|
||||||
|
currentPasswordInput = "",
|
||||||
|
passwordInput = "",
|
||||||
|
retypePasswordInput = "",
|
||||||
|
passwordHintInput = "",
|
||||||
|
)
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.x8bit.bitwarden.ui.auth.feature.resetPassword.util
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.createMockMasterPasswordPolicy
|
||||||
|
import com.x8bit.bitwarden.ui.auth.feature.resetpassword.util.toDisplayLabels
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class PolicyInformationMasterPasswordExtensionsTest {
|
||||||
|
@Test
|
||||||
|
fun `toDisplayLabels with multiple minLength values should choose highest value`() {
|
||||||
|
val policyList = listOf(
|
||||||
|
createMockMasterPasswordPolicy(minLength = null),
|
||||||
|
createMockMasterPasswordPolicy(minLength = 10),
|
||||||
|
createMockMasterPasswordPolicy(minLength = 2),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(R.string.policy_in_effect_min_length.asText(10)),
|
||||||
|
policyList.toDisplayLabels(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toDisplayLabels with multiple minComplexity values should choose highest value`() {
|
||||||
|
val policyList = listOf(
|
||||||
|
createMockMasterPasswordPolicy(minComplexity = null),
|
||||||
|
createMockMasterPasswordPolicy(minComplexity = 1),
|
||||||
|
createMockMasterPasswordPolicy(minComplexity = 2),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(R.string.policy_in_effect_min_complexity.asText(2)),
|
||||||
|
policyList.toDisplayLabels(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toDisplayLabels lists any nonNull requirements`() {
|
||||||
|
val policyList = listOf(
|
||||||
|
createMockMasterPasswordPolicy(requireUpper = true),
|
||||||
|
createMockMasterPasswordPolicy(requireLower = true),
|
||||||
|
createMockMasterPasswordPolicy(requireNumbers = true),
|
||||||
|
createMockMasterPasswordPolicy(requireSpecial = true),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
R.string.policy_in_effect_uppercase.asText(),
|
||||||
|
R.string.policy_in_effect_lowercase.asText(),
|
||||||
|
R.string.policy_in_effect_numbers.asText(),
|
||||||
|
R.string.policy_in_effect_special.asText(),
|
||||||
|
),
|
||||||
|
policyList.toDisplayLabels(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,6 +117,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -151,6 +152,7 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = true,
|
isBiometricsEnabled = true,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -728,6 +730,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -82,6 +82,15 @@ class RootNavScreenTest : BaseComposeTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure navigating to reset password works as expected:
|
||||||
|
rootNavStateFlow.value = RootNavState.ResetPassword
|
||||||
|
composeTestRule.runOnIdle {
|
||||||
|
fakeNavHostController.assertLastNavigation(
|
||||||
|
route = "reset_password",
|
||||||
|
navOptions = expectedNavOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure navigating to vault unlocked works as expected:
|
// Make sure navigating to vault unlocked works as expected:
|
||||||
rootNavStateFlow.value = RootNavState.VaultUnlocked(activeUserId = "userId")
|
rootNavStateFlow.value = RootNavState.VaultUnlocked(activeUserId = "userId")
|
||||||
composeTestRule.runOnIdle {
|
composeTestRule.runOnIdle {
|
||||||
|
|
|
@ -31,7 +31,6 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
@Test
|
||||||
fun `when the active user is not logged in the nav state should be Auth`() {
|
fun `when the active user is not logged in the nav state should be Auth`() {
|
||||||
mutableUserStateFlow.tryEmit(
|
mutableUserStateFlow.tryEmit(
|
||||||
|
@ -48,6 +47,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = false,
|
isLoggedIn = false,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -58,6 +58,33 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
assertEquals(RootNavState.Auth, viewModel.stateFlow.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when the active user needs a password reset the nav state should be ResetPassword`() {
|
||||||
|
mutableUserStateFlow.tryEmit(
|
||||||
|
UserState(
|
||||||
|
activeUserId = "activeUserId",
|
||||||
|
accounts = listOf(
|
||||||
|
UserState.Account(
|
||||||
|
userId = "activeUserId",
|
||||||
|
name = "name",
|
||||||
|
email = "email",
|
||||||
|
avatarColorHex = "avatarColorHex",
|
||||||
|
environment = Environment.Us,
|
||||||
|
isPremium = true,
|
||||||
|
isLoggedIn = false,
|
||||||
|
isVaultUnlocked = false,
|
||||||
|
isVaultPendingUnlock = true,
|
||||||
|
needsPasswordReset = true,
|
||||||
|
isBiometricsEnabled = false,
|
||||||
|
organizations = emptyList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(RootNavState.ResetPassword, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `when the active user but there are pending account additions the nav state should be Auth`() {
|
fun `when the active user but there are pending account additions the nav state should be Auth`() {
|
||||||
|
@ -75,6 +102,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -102,6 +130,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = true,
|
isVaultPendingUnlock = true,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -131,6 +160,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -166,6 +196,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -205,6 +236,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -237,6 +269,7 @@ class RootNavViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -975,6 +975,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -193,6 +193,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1787,6 +1787,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1003,6 +1003,7 @@ class AddSendViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2140,6 +2140,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = false,
|
isLoggedIn = false,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
id = "organizationId",
|
id = "organizationId",
|
||||||
|
|
|
@ -554,6 +554,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1498,6 +1498,7 @@ class VaultItemViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -1373,6 +1373,7 @@ private val DEFAULT_ACCOUNT = UserState.Account(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -423,6 +423,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
|
|
@ -95,6 +95,7 @@ private fun createMockUserState(hasOrganizations: Boolean = true): UserState =
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = if (hasOrganizations) {
|
organizations = if (hasOrganizations) {
|
||||||
listOf(
|
listOf(
|
||||||
|
|
|
@ -163,6 +163,7 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -1281,6 +1282,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
@ -1294,6 +1296,7 @@ private val DEFAULT_USER_STATE = UserState(
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -70,6 +70,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -88,6 +89,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -110,6 +112,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -132,6 +135,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = false,
|
isLoggedIn = false,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -169,6 +173,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -204,6 +209,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -243,6 +249,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
@ -270,6 +277,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = emptyList(),
|
organizations = emptyList(),
|
||||||
)
|
)
|
||||||
|
@ -306,6 +314,7 @@ class UserStateExtensionsTest {
|
||||||
isLoggedIn = true,
|
isLoggedIn = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
isVaultPendingUnlock = false,
|
isVaultPendingUnlock = false,
|
||||||
|
needsPasswordReset = false,
|
||||||
isBiometricsEnabled = false,
|
isBiometricsEnabled = false,
|
||||||
organizations = listOf(
|
organizations = listOf(
|
||||||
Organization(
|
Organization(
|
||||||
|
|
Loading…
Reference in a new issue