Skip to content

Commit

Permalink
BIT-868 Keep password fields on create account in sync (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahaisting-livefront authored and vvolkgang committed Jun 20, 2024
1 parent 9483d58 commit 77f693e
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
Expand Down Expand Up @@ -124,8 +127,11 @@ fun CreateAccountScreen(
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
var showPassword by rememberSaveable { mutableStateOf(false) }
BitwardenPasswordField(
label = stringResource(id = R.string.master_password),
showPassword = showPassword,
showPasswordChange = { showPassword = it },
value = state.passwordInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(PasswordInputChange(it)) }
Expand All @@ -138,6 +144,8 @@ fun CreateAccountScreen(
BitwardenPasswordField(
label = stringResource(id = R.string.retype_master_password),
value = state.confirmPasswordInput,
showPassword = showPassword,
showPasswordChange = { showPassword = it },
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,26 @@ import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R

/**
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
* internally.
* Represents a Bitwarden-styled password field that hoists show/hide password state to the caller.
*
* See overloaded [BitwardenPasswordField] for self managed show/hide state.
*
* @param label Label for the text field.
* @param value Current next on the text field.
* @param showPassword Whether or not password should be shown.
* @param showPasswordChange Lambda that is called when user request show/hide be toggled.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param initialShowPassword The initial state of the show/hide password control. A value of
* `false` (the default) indicates that that password should begin in the hidden state.
*/
@Composable
fun BitwardenPasswordField(
label: String,
value: String,
showPassword: Boolean,
showPasswordChange: (Boolean) -> Unit,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
initialShowPassword: Boolean = false,
) {
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
OutlinedTextField(
modifier = modifier,
textStyle = MaterialTheme.typography.bodyLarge,
Expand All @@ -55,7 +56,7 @@ fun BitwardenPasswordField(
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
IconButton(
onClick = { showPassword = !showPassword },
onClick = { showPasswordChange.invoke(!showPassword) },
) {
if (showPassword) {
Icon(
Expand All @@ -75,6 +76,36 @@ fun BitwardenPasswordField(
)
}

/**
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
* internally.
*
* @param label Label for the text field.
* @param value Current next on the text field.
* @param onValueChange Callback that is triggered when the password changes.
* @param modifier Modifier for the composable.
* @param initialShowPassword The initial state of the show/hide password control. A value of
* `false` (the default) indicates that that password should begin in the hidden state.
*/
@Composable
fun BitwardenPasswordField(
label: String,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
initialShowPassword: Boolean = false,
) {
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
BitwardenPasswordField(
modifier = modifier,
label = label,
value = value,
showPassword = showPassword,
showPasswordChange = { showPassword = !showPassword },
onValueChange = onValueChange,
)
}

@Preview
@Composable
private fun BitwardenPasswordField_preview_withInput_hidePassword() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount

import android.content.Intent
import android.net.Uri
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.filterToOne
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
Expand Down Expand Up @@ -259,6 +261,41 @@ class CreateAccountScreenTest : BaseComposeTest() {
composeTestRule.onNode(isDialog()).assertIsDisplayed()
}

@Test
fun `toggling one password field visibility should toggle the other`() {
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
every { eventFlow } returns emptyFlow()
}
composeTestRule.setContent {
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
}

// should start with 2 Show buttons:
composeTestRule
.onAllNodesWithContentDescription("Show")
.assertCountEquals(2)
.get(0)
.performClick()

// after clicking there should be no Show buttons:
composeTestRule
.onAllNodesWithContentDescription("Show")
.assertCountEquals(0)

// and there should be 2 hide buttons now, and we'll click the second one:
composeTestRule
.onAllNodesWithContentDescription("Hide")
.assertCountEquals(2)
.get(1)
.performClick()

// then there should be two show buttons again
composeTestRule
.onAllNodesWithContentDescription("Show")
.assertCountEquals(2)
}

companion object {
private const val TEST_INPUT = "input"
private val DEFAULT_STATE = CreateAccountState(
Expand Down

0 comments on commit 77f693e

Please sign in to comment.