From b2391dd66afe54472ae08f288d40fe0a55744a6c Mon Sep 17 00:00:00 2001
From: Phil Cappelli <150719757+phil-livefront@users.noreply.github.com>
Date: Mon, 2 Dec 2024 14:38:45 -0500
Subject: [PATCH] PM-15147 - Design Audit - Master Password Guidance Screen
(#4383)
---
.../CompleteRegistrationScreen.kt | 5 +
.../MasterPasswordGuidanceScreen.kt | 226 +++++++++---------
.../components/card/BitwardenActionCard.kt | 29 ++-
.../card/BitwardenActionCardSmall.kt | 8 +-
.../card/color/BitwardenCardColors.kt | 20 +-
app/src/main/res/values/strings.xml | 13 +
.../MasterPasswordGuidanceScreenTest.kt | 4 +-
7 files changed, 173 insertions(+), 132 deletions(-)
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 f96a9b6333..05e87495e7 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
@@ -46,6 +46,7 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
+import com.x8bit.bitwarden.ui.platform.components.card.color.bitwardenCardColors
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
@@ -211,6 +212,10 @@ private fun CompleteRegistrationContent(
actionIcon = rememberVectorPainter(id = R.drawable.ic_question_circle),
actionText = stringResource(id = R.string.what_makes_a_password_strong),
callToActionText = stringResource(id = R.string.learn_more),
+ callToActionTextColor = BitwardenTheme.colorScheme.text.interaction,
+ colors = bitwardenCardColors(
+ containerColor = BitwardenTheme.colorScheme.background.primary,
+ ),
onCardClicked = handler.onMakeStrongPassword,
modifier = Modifier
.fillMaxWidth()
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt
index 6b2befc447..e6e164dd31 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreen.kt
@@ -1,52 +1,46 @@
package com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
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.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
+import com.x8bit.bitwarden.ui.platform.base.util.bitwardenBoldSpanStyle
+import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
+import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
-import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
-import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
+import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
+import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
+import com.x8bit.bitwarden.ui.platform.components.model.ContentBlockData
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
-
-private const val BULLET_TWO_TAB = "\u2022\t\t"
+import kotlinx.collections.immutable.persistentListOf
/**
* The top level composable for the Master Password Guidance screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
-@Suppress("LongMethod")
@Composable
fun MasterPasswordGuidanceScreen(
onNavigateBack: () -> Unit,
@@ -81,126 +75,126 @@ fun MasterPasswordGuidanceScreen(
)
},
) {
- Column(
+ MasterPasswordGuidanceContent(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.standardHorizontalMargin(),
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(size = 4.dp))
- .background(BitwardenTheme.colorScheme.background.tertiary),
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(all = 24.dp),
- ) {
-
- Text(
- text = stringResource(R.string.what_makes_a_password_strong),
- style = BitwardenTheme.typography.titleMedium,
- color = BitwardenTheme.colorScheme.text.primary,
- )
- Spacer(modifier = Modifier.height(8.dp))
- Text(
- style = BitwardenTheme.typography.bodyMedium,
- color = BitwardenTheme.colorScheme.text.primary,
- text = stringResource(
- R.string.the_longer_your_password_the_more_difficult_to_hack,
- ),
- )
- }
- BitwardenHorizontalDivider()
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 24.dp, vertical = 16.dp),
- ) {
- Text(
- text = stringResource(R.string.the_strongest_passwords_are_usually),
- style = BitwardenTheme.typography.titleSmall,
- color = BitwardenTheme.colorScheme.text.primary,
- )
- Spacer(modifier = Modifier.height(8.dp))
- BulletTextRow(text = stringResource(R.string.twelve_or_more_characters))
- BulletTextRow(
- text = stringResource(
- R.string.random_and_complex_using_numbers_and_special_characters,
- ),
- )
- BulletTextRow(
- text = stringResource(R.string.totally_different_from_your_other_passwords),
- )
- }
- }
- Spacer(modifier = Modifier.height(16.dp))
- TryGeneratorCard(
- onCardClicked = remember(viewModel) {
- {
- viewModel.trySendAction(
- MasterPasswordGuidanceAction.TryPasswordGeneratorAction,
- )
- }
- },
- )
- Spacer(modifier = Modifier.navigationBarsPadding())
- }
+ onTryPasswordGeneratorAction = {
+ viewModel.trySendAction(
+ MasterPasswordGuidanceAction.TryPasswordGeneratorAction,
+ )
+ },
+ )
}
}
@Composable
-private fun TryGeneratorCard(
- onCardClicked: () -> Unit,
+private fun MasterPasswordGuidanceContent(
modifier: Modifier = Modifier,
+ onTryPasswordGeneratorAction: () -> Unit,
) {
- BitwardenActionCardSmall(
- actionIcon = rememberVectorPainter(id = R.drawable.ic_generate),
- actionText = stringResource(
- R.string.use_the_generator_to_create_a_strong_unique_password,
- ),
- callToActionText = stringResource(R.string.try_it_out),
- onCardClicked = onCardClicked,
- modifier = modifier
- .fillMaxWidth(),
- trailingContent = {
- Icon(
- painter = rememberVectorPainter(id = R.drawable.ic_chevron_right),
- contentDescription = null,
- tint = BitwardenTheme.colorScheme.icon.primary,
- modifier = Modifier
- .align(Alignment.Center)
- .size(16.dp),
- )
- },
- )
-}
-
-@Composable
-private fun BulletTextRow(
- text: String,
- modifier: Modifier = Modifier,
-) {
- Row(
- modifier = modifier
- .fillMaxWidth()
- .padding(horizontal = 8.dp),
- ) {
+ Column(modifier = modifier) {
+ Spacer(modifier = Modifier.height(24.dp))
Text(
- text = BULLET_TWO_TAB,
+ text = stringResource(R.string.a_secure_memorable_password),
+ textAlign = TextAlign.Center,
+ style = BitwardenTheme.typography.titleMedium,
+ color = BitwardenTheme.colorScheme.text.primary,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = stringResource(
+ R.string.one_of_the_best_ways_to_create_a_secure_and_memorable_password,
+ ),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
- modifier = Modifier.clearAndSetSemantics { },
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
)
- Text(
- text = text,
- style = BitwardenTheme.typography.bodyMedium,
- color = BitwardenTheme.colorScheme.text.primary,
+ Spacer(modifier = Modifier.height(24.dp))
+ MasterPasswordGuidanceContentBlocks()
+ NeedSomeInspirationCard(
+ onActionClicked = {
+ onTryPasswordGeneratorAction()
+ },
+ )
+ Spacer(modifier = Modifier.navigationBarsPadding())
+ }
+}
+
+@Composable
+private fun MasterPasswordGuidanceContentBlocks(modifier: Modifier = Modifier) {
+ Column(modifier = modifier) {
+ BitwardenContentCard(
+ contentItems = persistentListOf(
+ ContentBlockData(
+ headerText = stringResource(R.string.choose_three_or_four_random_words)
+ .toAnnotatedString(),
+ subtitleText = createAnnotatedString(
+ mainString = stringResource(
+ R.string.pick_three_or_four_random_unrelated_words,
+ ),
+ highlights = listOf(
+ stringResource(
+ R.string.pick_three_or_four_random_unrelated_words_highlight,
+ ),
+ ),
+ highlightStyle = bitwardenBoldSpanStyle,
+ ),
+ iconVectorResource = R.drawable.ic_number1,
+ ),
+ ContentBlockData(
+ headerText = stringResource(R.string.combine_those_words_together)
+ .toAnnotatedString(),
+ subtitleText = createAnnotatedString(
+ mainString = stringResource(
+ R.string.put_the_words_together_in_any_order_to_form_your_passphrase,
+ ),
+ highlights = listOf(
+ stringResource(
+ R.string.use_hyphens_spaces_or_leave_them_as_long_word_highlight,
+ ),
+ ),
+ highlightStyle = bitwardenBoldSpanStyle,
+ ),
+ iconVectorResource = R.drawable.ic_number2,
+ ),
+ ContentBlockData(
+ headerText = stringResource(R.string.make_it_yours).toAnnotatedString(),
+ subtitleText = createAnnotatedString(
+ mainString = stringResource(
+ R.string.add_a_number_or_symbol_to_make_it_even_stronger,
+ ),
+ highlights = listOf(
+ stringResource(R.string.add_a_number_or_symbol_highlight),
+ ),
+ highlightStyle = bitwardenBoldSpanStyle,
+ ),
+ iconVectorResource = R.drawable.ic_number3,
+ ),
+ ),
)
}
+ Spacer(modifier = Modifier.height(24.dp))
+}
+
+@Composable
+private fun NeedSomeInspirationCard(
+ onActionClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ BitwardenActionCard(
+ cardTitle = stringResource(R.string.need_some_inspiration),
+ actionText = stringResource(R.string.check_out_the_passphrase_generator),
+ onActionClick = onActionClicked,
+ modifier = modifier.fillMaxWidth(),
+ )
}
@Preview
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt
index abeaf0c364..e4976e15f5 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCard.kt
@@ -35,7 +35,7 @@ import kotlin.let
* @param cardTitle The title of the card.
* @param actionText The text content on the CTA button.
* @param onActionClick The action to perform when the CTA button is clicked.
- * @param onDismissClick The action to perform when the dismiss button is clicked.
+ * @param onDismissClick Optional action to perform when the dismiss button is clicked.
* @param leadingContent Optional content to display on the leading side of the
* [cardTitle] [Text].
*/
@@ -44,8 +44,8 @@ fun BitwardenActionCard(
cardTitle: String,
actionText: String,
onActionClick: () -> Unit,
- onDismissClick: () -> Unit,
modifier: Modifier = Modifier,
+ onDismissClick: (() -> Unit)? = null,
cardSubtitle: String? = null,
leadingContent: @Composable (() -> Unit)? = null,
) {
@@ -69,11 +69,13 @@ fun BitwardenActionCard(
)
}
Spacer(Modifier.weight(1f))
- BitwardenStandardIconButton(
- painter = rememberVectorPainter(id = R.drawable.ic_close),
- contentDescription = stringResource(id = R.string.close),
- onClick = onDismissClick,
- )
+ onDismissClick?.let {
+ BitwardenStandardIconButton(
+ painter = rememberVectorPainter(id = R.drawable.ic_close),
+ contentDescription = stringResource(id = R.string.close),
+ onClick = it,
+ )
+ }
}
cardSubtitle?.let {
Spacer(Modifier.height(4.dp))
@@ -103,6 +105,19 @@ fun BitwardenActionCard(
*/
fun actionCardExitAnimation() = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top)
+@Preview
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun BitwardenActionCardWithSubtitleNoDismiss_preview() {
+ BitwardenTheme {
+ BitwardenActionCard(
+ cardTitle = "Title",
+ actionText = "Action",
+ onActionClick = {},
+ )
+ }
+}
+
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt
index df23460901..d8611c9565 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/BitwardenActionCardSmall.kt
@@ -12,12 +12,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
+import androidx.compose.material3.CardColors
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.VectorPainter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -34,6 +36,8 @@ fun BitwardenActionCardSmall(
actionIcon: VectorPainter,
actionText: String,
callToActionText: String,
+ callToActionTextColor: Color = BitwardenTheme.colorScheme.text.primary,
+ colors: CardColors = bitwardenCardColors(),
onCardClicked: () -> Unit,
modifier: Modifier = Modifier,
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
@@ -42,7 +46,7 @@ fun BitwardenActionCardSmall(
onClick = onCardClicked,
shape = BitwardenTheme.shapes.actionCard,
modifier = modifier,
- colors = bitwardenCardColors(),
+ colors = colors,
elevation = CardDefaults.elevatedCardElevation(),
border = BorderStroke(width = 1.dp, color = BitwardenTheme.colorScheme.stroke.border),
) {
@@ -70,7 +74,7 @@ fun BitwardenActionCardSmall(
Text(
text = callToActionText,
style = BitwardenTheme.typography.labelLarge,
- color = BitwardenTheme.colorScheme.text.primary,
+ color = callToActionTextColor,
)
}
Spacer(modifier = Modifier.width(16.dp))
diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/color/BitwardenCardColors.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/color/BitwardenCardColors.kt
index 05e9995995..60ce38dea8 100644
--- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/color/BitwardenCardColors.kt
+++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/card/color/BitwardenCardColors.kt
@@ -2,15 +2,23 @@ package com.x8bit.bitwarden.ui.platform.components.card.color
import androidx.compose.material3.CardColors
import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Provides a default set of Bitwarden-styled colors for a card.
*/
@Composable
-fun bitwardenCardColors(): CardColors = CardColors(
- containerColor = BitwardenTheme.colorScheme.background.tertiary,
- contentColor = BitwardenTheme.colorScheme.text.primary,
- disabledContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
- disabledContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
-)
+fun bitwardenCardColors(
+ containerColor: Color = BitwardenTheme.colorScheme.background.tertiary,
+ contentColor: Color = BitwardenTheme.colorScheme.text.primary,
+ disabledContainerColor: Color = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
+ disabledContentColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
+): CardColors {
+ return CardColors(
+ containerColor = containerColor,
+ contentColor = contentColor,
+ disabledContainerColor = disabledContainerColor,
+ disabledContentColor = disabledContentColor,
+ )
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 22540f6b0b..a4ab85954c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -962,6 +962,7 @@ Do you want to switch to this account?
Remove passkey
Passkey removed
What makes a password strong?
+ A secure, memorable password
The longer your password, the more difficult it is to hack. The minimum for account creation is 12 characters but if you do 14 characters, the time to crack your password would be centuries!
The strongest passwords are usually:
12 or more characters
@@ -978,6 +979,7 @@ Do you want to switch to this account?
Set up biometrics or choose a PIN code to quickly access your vault and AutoFill your logins.
Never lose access to your vault
The best way to make sure you can always access your account is to set up safeguards from the start.
+ One of the best ways to create a secure and memorable password is to use a passphrase. \nHere’s how:
Create a hint
Your hint will be sent to you via email when you request it.
Write your password down
@@ -1099,4 +1101,15 @@ Do you want to switch to this account?
You’ve been logged out because your device’s biometrics don’t meet the latest security requirements. To update settings, log in once again or contact your administrator for access.
CXP Import
CXP Export
+ Choose three or four random words
+ Pick three or four random, unrelated words that you can easily remember. Think of objects, places, or things you like.
+ objects, places, or things
+ Combine those words together
+ Put the words together in any order to form your passphrase. Use hyphens, spaces, or leave them as one long word—your choice!
+ Use hyphens, spaces, or leave them as one long word
+ Make it yours
+ Add a number or symbol to make it even stronger. Now you have a unique, secure, and memorable passphrase!
+ Add a number or symbol
+ "Need some inspiration?"
+ "Check out the passphrase generator"
diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreenTest.kt
index 26ff59f0b5..66a1913ee1 100644
--- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreenTest.kt
+++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/masterpasswordguidance/MasterPasswordGuidanceScreenTest.kt
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performScrollTo
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
@@ -45,7 +46,8 @@ class MasterPasswordGuidanceScreenTest : BaseComposeTest() {
@Test
fun `Generator card click should invoke send of TryPasswordGeneratorAction`() {
composeTestRule
- .onNodeWithText("Use the generator to create a strong, unique password")
+ .onNodeWithText("Check out the passphrase generator")
+ .performScrollTo()
.performClick()
verify { viewModel.trySendAction(MasterPasswordGuidanceAction.TryPasswordGeneratorAction) }