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()
+ }
+}