From 0d6aeee870e9b34656d39b4ebd757918a93b4ddd Mon Sep 17 00:00:00 2001 From: Dave Severns <149429124+dseverns-livefront@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:13:23 -0400 Subject: [PATCH] PM-10617 modify pw strength indicator to show min chars if required. (#3793) --- .../CompleteRegistrationScreen.kt | 9 +- .../CompleteRegistrationViewModel.kt | 2 + .../PasswordStrengthIndicator.kt | 107 +++++++++++++++++- .../createaccount/CreateAccountScreen.kt | 1 + .../settings/exportvault/ExportVaultScreen.kt | 1 + app/src/main/res/drawable/ic_circle.xml | 11 ++ .../main/res/drawable/ic_plain_checkmark.xml | 13 +++ .../CompleteRegistrationScreenTest.kt | 1 + .../CompleteRegistrationViewModelTest.kt | 2 + .../PasswordStrengthIndicatorTest.kt | 42 +++++++ 10 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/drawable/ic_circle.xml create mode 100644 app/src/main/res/drawable/ic_plain_checkmark.xml create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicatorTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt index 309b6fbf04..09660d728e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreen.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index f925ed2126..f1969a23ee 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -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 { /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt index eb2fc2cef9..8c7507f7f1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicator.kt @@ -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, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt index 69eeb69443..2591704849 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreen.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt index 4fc52e1169..077b6f1e69 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreen.kt @@ -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)) diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 0000000000..84191504b4 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_plain_checkmark.xml b/app/src/main/res/drawable/ic_plain_checkmark.xml new file mode 100644 index 0000000000..f93fc5a508 --- /dev/null +++ b/app/src/main/res/drawable/ic_plain_checkmark.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt index 59fe6ab18a..9d7293c688 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationScreenTest.kt @@ -350,6 +350,7 @@ class CompleteRegistrationScreenTest : BaseComposeTest() { dialog = null, passwordStrengthState = PasswordStrengthState.NONE, onBoardingEnabled = false, + minimumPasswordLength = 12, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt index 617c1a3138..a40a7d6c51 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModelTest.kt @@ -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, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicatorTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicatorTest.kt new file mode 100644 index 0000000000..0196f8771a --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/PasswordStrengthIndicatorTest.kt @@ -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() + } +}