1
0
Fork 0
mirror of https://github.com/bitwarden/android.git synced 2025-03-09 15:58:53 +03:00

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

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
app/src
main
java/com/x8bit/bitwarden/ui
res/drawable
test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration

View file

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

View file

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

View file

@ -1,25 +1,36 @@
package com.x8bit.bitwarden.ui.auth.feature.completeregistration package com.x8bit.bitwarden.ui.auth.feature.completeregistration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer 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 androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.asText 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 import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
/** /**
@ -30,6 +41,8 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
fun PasswordStrengthIndicator( fun PasswordStrengthIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: PasswordStrengthState, state: PasswordStrengthState,
currentCharacterCount: Int,
minimumCharacterCount: Int? = null,
) { ) {
val widthPercent by animateFloatAsState( val widthPercent by animateFloatAsState(
targetValue = when (state) { targetValue = when (state) {
@ -85,10 +98,65 @@ fun PasswordStrengthIndicator(
) )
} }
Spacer(Modifier.height(4.dp)) 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(
text = label(), text = stringResource(R.string.minimum_characters, minimumCharacterCount),
color = characterCountColor,
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = indicatorColor,
) )
} }
} }
@ -104,3 +172,38 @@ enum class PasswordStrengthState {
GOOD, GOOD,
STRONG, 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( PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState, state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
BitwardenPasswordField( BitwardenPasswordField(

View file

@ -256,6 +256,7 @@ private fun ExportVaultScreenContent(
PasswordStrengthIndicator( PasswordStrengthIndicator(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
state = state.passwordStrengthState, state = state.passwordStrengthState,
currentCharacterCount = state.passwordInput.length,
) )
Spacer(modifier = Modifier.height(4.dp)) 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, dialog = null,
passwordStrengthState = PasswordStrengthState.NONE, passwordStrengthState = PasswordStrengthState.NONE,
onBoardingEnabled = false, onBoardingEnabled = false,
minimumPasswordLength = 12,
) )
} }
} }

View file

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