PM-10617 modify pw strength indicator to show min chars if required. (#3793)

This commit is contained in:
Dave Severns 2024-08-22 11:13:23 -04:00 committed by GitHub
parent a0a5070ac7
commit 0d6aeee870
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 184 additions and 5 deletions

View file

@ -160,6 +160,7 @@ fun CompleteRegistrationScreen(
modifier = Modifier.standardHorizontalMargin(),
nextButtonEnabled = state.hasValidMasterPassword,
callToActionText = state.callToActionText(),
minimumPasswordLength = state.minimumPasswordLength,
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
@ -175,6 +176,7 @@ private fun CompleteRegistrationContent(
passwordHintInput: String,
isCheckDataBreachesToggled: Boolean,
nextButtonEnabled: Boolean,
minimumPasswordLength: Int,
callToActionText: String,
handler: CompleteRegistrationHandler,
modifier: Modifier = Modifier,
@ -212,8 +214,9 @@ private fun CompleteRegistrationContent(
)
Spacer(modifier = Modifier.height(8.dp))
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = passwordStrengthState,
currentCharacterCount = passwordInput.length,
minimumCharacterCount = minimumPasswordLength,
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenPasswordField(
@ -266,7 +269,6 @@ private fun CompleteRegistrationContentHeader(
modifier: Modifier = Modifier,
configuration: Configuration = LocalConfiguration.current,
) {
if (configuration.isPortrait) {
Column(
modifier = modifier,
@ -319,7 +321,7 @@ private fun OrderedHeaderContent() {
@PreviewScreenSizes
@Composable
private fun CompleteRegistrationContentPreview() {
private fun CompleteRegistrationContent_preview() {
BitwardenTheme {
CompleteRegistrationContent(
passwordInput = "tortor",
@ -342,6 +344,7 @@ private fun CompleteRegistrationContentPreview() {
callToActionText = "Next",
nextButtonEnabled = true,
modifier = Modifier.standardHorizontalMargin(),
minimumPasswordLength = 12,
)
}
}

View file

@ -65,6 +65,7 @@ class CompleteRegistrationViewModel @Inject constructor(
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = featureFlagManager.getFeatureFlag(FlagKey.OnboardingFlow),
minimumPasswordLength = MIN_PASSWORD_LENGTH,
)
},
) {
@ -340,6 +341,7 @@ data class CompleteRegistrationState(
val dialog: CompleteRegistrationDialog?,
val passwordStrengthState: PasswordStrengthState,
val onBoardingEnabled: Boolean,
val minimumPasswordLength: Int,
) : Parcelable {
/**

View file

@ -1,25 +1,36 @@
package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
/**
@ -30,6 +41,8 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
fun PasswordStrengthIndicator(
modifier: Modifier = Modifier,
state: PasswordStrengthState,
currentCharacterCount: Int,
minimumCharacterCount: Int? = null,
) {
val widthPercent by animateFloatAsState(
targetValue = when (state) {
@ -85,10 +98,65 @@ fun PasswordStrengthIndicator(
)
}
Spacer(Modifier.height(4.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
minimumCharacterCount?.let { minCount ->
MinimumCharacterCount(
minimumRequirementMet = currentCharacterCount >= minCount,
minimumCharacterCount = minCount,
)
}
Text(
text = label(),
style = MaterialTheme.typography.labelSmall,
color = indicatorColor,
)
}
}
}
@Composable
private fun MinimumCharacterCount(
modifier: Modifier = Modifier,
minimumRequirementMet: Boolean,
minimumCharacterCount: Int,
) {
val nonMaterialColors = LocalNonMaterialColors.current
val characterCountColor by animateColorAsState(
targetValue = if (minimumRequirementMet) {
nonMaterialColors.passwordStrong
} else {
MaterialTheme.colorScheme.surfaceDim
},
label = "minmumCharacterCountColor",
)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedContent(
targetState = if (minimumRequirementMet) {
R.drawable.ic_plain_checkmark
} else {
R.drawable.ic_circle
},
label = "iconForMinimumCharacterCount",
) {
Icon(
painter = rememberVectorPainter(id = it),
contentDescription = null,
tint = characterCountColor,
modifier = Modifier.size(12.dp),
)
}
Spacer(modifier = Modifier.width(2.dp))
Text(
text = label(),
text = stringResource(R.string.minimum_characters, minimumCharacterCount),
color = characterCountColor,
style = MaterialTheme.typography.labelSmall,
color = indicatorColor,
)
}
}
@ -104,3 +172,38 @@ enum class PasswordStrengthState {
GOOD,
STRONG,
}
@Preview(showBackground = true)
@Composable
private fun PasswordStrengthIndicatorPreview_minCharMet() {
BitwardenTheme {
PasswordStrengthIndicator(
state = PasswordStrengthState.WEAK_3,
currentCharacterCount = 12,
minimumCharacterCount = 12,
)
}
}
@Preview(showBackground = true)
@Composable
private fun PasswordStrengthIndicatorPreview_minCharNotMet() {
BitwardenTheme {
PasswordStrengthIndicator(
state = PasswordStrengthState.WEAK_3,
currentCharacterCount = 11,
minimumCharacterCount = 12,
)
}
}
@Preview(showBackground = true)
@Composable
private fun PasswordStrengthIndicatorPreview_noMinChar() {
BitwardenTheme {
PasswordStrengthIndicator(
state = PasswordStrengthState.WEAK_3,
currentCharacterCount = 12,
)
}
}

View file

@ -228,6 +228,7 @@ fun CreateAccountScreen(
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
)
Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField(

View file

@ -256,6 +256,7 @@ private fun ExportVaultScreenContent(
PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
)
Spacer(modifier = Modifier.height(4.dp))

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:viewportWidth="12"
android:viewportHeight="12">
<path
android:pathData="M6,6m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#DBD9DD"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="10dp"
android:height="8dp"
android:viewportWidth="10"
android:viewportHeight="8">
<path
android:pathData="M1,4.429L3.88,7L9,1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#017E45"
android:strokeLineCap="round"/>
</vector>

View file

@ -350,6 +350,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() {
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = false,
minimumPasswordLength = 12,
)
}
}

View file

@ -539,6 +539,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = false,
minimumPasswordLength = 12,
)
private val VALID_INPUT_STATE = CompleteRegistrationState(
userEmail = EMAIL,
@ -551,6 +552,7 @@ class CompleteRegistrationViewModelTest : BaseViewModelTest() {
dialog = null,
passwordStrengthState = PasswordStrengthState.GOOD,
onBoardingEnabled = false,
minimumPasswordLength = 12,
)
}
}

View file

@ -0,0 +1,42 @@
package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import org.junit.Test
class PasswordStrengthIndicatorTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `PasswordStrengthIndicator with minimum character count met displays minimum character count`() {
composeTestRule.setContent {
PasswordStrengthIndicator(
state = PasswordStrengthState.WEAK_3,
currentCharacterCount = 12,
minimumCharacterCount = 12,
)
}
composeTestRule
.onNodeWithText("characters", substring = true)
.assertExists()
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `PasswordStrengthIndicator with no minimum character count met does not minimum character count`() {
composeTestRule.setContent {
PasswordStrengthIndicator(
state = PasswordStrengthState.WEAK_3,
currentCharacterCount = 12,
minimumCharacterCount = null,
)
}
composeTestRule
.onNodeWithText("characters", substring = true)
.assertDoesNotExist()
}
}