mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 18:36:32 +03:00
BIT-868 Keep password fields on create account in sync (#125)
This commit is contained in:
parent
9483d58d81
commit
77f693e159
3 changed files with 105 additions and 29 deletions
|
@ -23,7 +23,10 @@ import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
@ -124,8 +127,11 @@ fun CreateAccountScreen(
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
var showPassword by rememberSaveable { mutableStateOf(false) }
|
||||||
BitwardenPasswordField(
|
BitwardenPasswordField(
|
||||||
label = stringResource(id = R.string.master_password),
|
label = stringResource(id = R.string.master_password),
|
||||||
|
showPassword = showPassword,
|
||||||
|
showPasswordChange = { showPassword = it },
|
||||||
value = state.passwordInput,
|
value = state.passwordInput,
|
||||||
onValueChange = remember(viewModel) {
|
onValueChange = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(PasswordInputChange(it)) }
|
{ viewModel.trySendAction(PasswordInputChange(it)) }
|
||||||
|
@ -138,6 +144,8 @@ fun CreateAccountScreen(
|
||||||
BitwardenPasswordField(
|
BitwardenPasswordField(
|
||||||
label = stringResource(id = R.string.retype_master_password),
|
label = stringResource(id = R.string.retype_master_password),
|
||||||
value = state.confirmPasswordInput,
|
value = state.confirmPasswordInput,
|
||||||
|
showPassword = showPassword,
|
||||||
|
showPasswordChange = { showPassword = it },
|
||||||
onValueChange = remember(viewModel) {
|
onValueChange = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
|
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,6 +20,62 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenPasswordField(
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
showPassword: Boolean,
|
||||||
|
showPasswordChange: (Boolean) -> Unit,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = modifier,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge,
|
||||||
|
label = { Text(text = label) },
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
visualTransformation = if (showPassword) {
|
||||||
|
VisualTransformation.None
|
||||||
|
} else {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
},
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showPasswordChange.invoke(!showPassword) },
|
||||||
|
) {
|
||||||
|
if (showPassword) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_visibility_off),
|
||||||
|
contentDescription = stringResource(id = R.string.hide),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_visibility),
|
||||||
|
contentDescription = stringResource(id = R.string.show),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
|
* Represents a Bitwarden-styled password field that manages the state of a show/hide indicator
|
||||||
* internally.
|
* internally.
|
||||||
|
@ -40,38 +96,13 @@ fun BitwardenPasswordField(
|
||||||
initialShowPassword: Boolean = false,
|
initialShowPassword: Boolean = false,
|
||||||
) {
|
) {
|
||||||
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
|
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
|
||||||
OutlinedTextField(
|
BitwardenPasswordField(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
textStyle = MaterialTheme.typography.bodyLarge,
|
label = label,
|
||||||
label = { Text(text = label) },
|
|
||||||
value = value,
|
value = value,
|
||||||
|
showPassword = showPassword,
|
||||||
|
showPasswordChange = { showPassword = !showPassword },
|
||||||
onValueChange = onValueChange,
|
onValueChange = onValueChange,
|
||||||
visualTransformation = if (showPassword) {
|
|
||||||
VisualTransformation.None
|
|
||||||
} else {
|
|
||||||
PasswordVisualTransformation()
|
|
||||||
},
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = { showPassword = !showPassword },
|
|
||||||
) {
|
|
||||||
if (showPassword) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.ic_visibility_off),
|
|
||||||
contentDescription = stringResource(id = R.string.hide),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.ic_visibility),
|
|
||||||
contentDescription = stringResource(id = R.string.show),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,12 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.filterToOne
|
import androidx.compose.ui.test.filterToOne
|
||||||
import androidx.compose.ui.test.hasAnyAncestor
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
import androidx.compose.ui.test.isDialog
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
@ -259,6 +261,41 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.onNode(isDialog()).assertIsDisplayed()
|
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 {
|
companion object {
|
||||||
private const val TEST_INPUT = "input"
|
private const val TEST_INPUT = "input"
|
||||||
private val DEFAULT_STATE = CreateAccountState(
|
private val DEFAULT_STATE = CreateAccountState(
|
||||||
|
|
Loading…
Reference in a new issue