PM-28522: Update the Login With Device Screen (#6184)

This commit is contained in:
David Perez 2025-12-01 10:25:30 -06:00 committed by GitHub
parent f02b374e98
commit ca7a65fc95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 92 additions and 115 deletions

View File

@ -1,16 +1,11 @@
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding 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.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -20,7 +15,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
@ -30,13 +24,17 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.ui.platform.base.util.EventsEffect import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularProgressIndicator import com.bitwarden.ui.platform.components.field.BitwardenTextField
import com.bitwarden.ui.platform.components.field.model.TextToolbarType
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.text.BitwardenClickableText import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.resource.BitwardenString
@ -120,111 +118,99 @@ private fun LoginWithDeviceScreenContent(
modifier = modifier modifier = modifier
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
Spacer(modifier = Modifier.height(height = 24.dp))
Text( Text(
text = state.title(), text = state.title(),
textAlign = TextAlign.Start, textAlign = TextAlign.Center,
style = BitwardenTheme.typography.headlineMedium, style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary, color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .standardHorizontalMargin()
.fillMaxWidth(), .fillMaxWidth(),
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(height = 12.dp))
Text( Text(
text = state.subtitle(), text = state.subtitle(),
textAlign = TextAlign.Start, textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium, style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary, color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .standardHorizontalMargin()
.fillMaxWidth(), .fillMaxWidth(),
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(height = 12.dp))
Text( Text(
text = state.description(), text = state.description(),
textAlign = TextAlign.Start, textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium, style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary, color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .standardHorizontalMargin()
.fillMaxWidth(), .fillMaxWidth(),
) )
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
Text( BitwardenTextField(
text = stringResource(id = BitwardenString.fingerprint_phrase), label = stringResource(id = BitwardenString.fingerprint_phrase),
textAlign = TextAlign.Start, value = state.fingerprintPhrase,
style = BitwardenTheme.typography.titleLarge, textFieldTestTag = "FingerprintPhraseValue",
color = BitwardenTheme.colorScheme.text.primary, onValueChange = { },
readOnly = true,
singleLine = false,
textToolbarType = TextToolbarType.NONE,
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
textColor = BitwardenTheme.colorScheme.text.codePink,
cardStyle = CardStyle.Full,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = state.fingerprintPhrase,
textAlign = TextAlign.Start,
color = BitwardenTheme.colorScheme.text.codePink,
style = BitwardenTheme.typography.sensitiveInfoSmall,
minLines = 2,
modifier = Modifier
.testTag("FingerprintPhraseValue")
.padding(horizontal = 16.dp)
.fillMaxWidth(), .fillMaxWidth(),
) )
if (state.allowsResend) { if (state.allowsResend) {
Column( Spacer(modifier = Modifier.height(height = 24.dp))
verticalArrangement = Arrangement.Center, BitwardenOutlinedButton(
modifier = Modifier
.defaultMinSize(minHeight = 40.dp)
.align(Alignment.Start),
) {
if (state.isResendNotificationLoading) {
BitwardenCircularProgressIndicator(
modifier = Modifier
.padding(horizontal = 64.dp)
.size(size = 16.dp),
)
} else {
BitwardenClickableText(
modifier = Modifier.testTag("ResendNotificationButton"),
label = stringResource(id = BitwardenString.resend_notification), label = stringResource(id = BitwardenString.resend_notification),
style = BitwardenTheme.typography.labelLarge,
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
onClick = onResendNotificationClick, onClick = onResendNotificationClick,
modifier = Modifier
.testTag(tag = "ResendNotificationButton")
.standardHorizontalMargin()
.fillMaxWidth(),
) )
} }
}
}
Spacer(modifier = Modifier.height(28.dp)) Spacer(modifier = Modifier.height(height = 24.dp))
Text( Text(
text = state.otherOptions(), text = state.otherOptions(),
textAlign = TextAlign.Start, textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium, style = BitwardenTheme.typography.bodySmall,
color = BitwardenTheme.colorScheme.text.primary, color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier modifier = Modifier
.padding(horizontal = 16.dp) .standardHorizontalMargin()
.fillMaxWidth(), .fillMaxWidth(),
) )
BitwardenClickableText( Spacer(modifier = Modifier.height(height = 12.dp))
modifier = Modifier.testTag("ViewAllLoginOptionsButton"),
label = stringResource(id = BitwardenString.view_all_login_options), BitwardenHyperTextLink(
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), annotatedResId = BitwardenString.need_another_option_view_all_login_options,
style = BitwardenTheme.typography.labelLarge, annotationKey = "viewAll",
accessibilityString = stringResource(id = BitwardenString.view_all_login_options),
onClick = onViewAllLogInOptionsClick, onClick = onViewAllLogInOptionsClick,
style = BitwardenTheme.typography.bodySmall,
modifier = Modifier
.testTag(tag = "ViewAllLoginOptionsButton")
.standardHorizontalMargin()
.fillMaxWidth(),
) )
Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding()) Spacer(modifier = Modifier.navigationBarsPadding())
} }
} }

View File

@ -52,7 +52,7 @@ class LoginWithDeviceViewModel @Inject constructor(
private var authJob: Job = Job().apply { complete() } private var authJob: Job = Job().apply { complete() }
init { init {
sendNewAuthRequest(isResend = false) sendNewAuthRequest()
} }
override fun handleAction(action: LoginWithDeviceAction) { override fun handleAction(action: LoginWithDeviceAction) {
@ -74,7 +74,14 @@ class LoginWithDeviceViewModel @Inject constructor(
} }
private fun handleResendNotificationClicked() { private fun handleResendNotificationClicked() {
sendNewAuthRequest(isResend = true) mutableStateFlow.update {
it.copy(
dialogState = LoginWithDeviceState.DialogState.Loading(
message = BitwardenString.resending.asText(),
),
)
}
sendNewAuthRequest()
} }
private fun handleViewAllLogInOptionsClicked() { private fun handleViewAllLogInOptionsClicked() {
@ -99,9 +106,6 @@ class LoginWithDeviceViewModel @Inject constructor(
) { ) {
when (val result = action.result) { when (val result = action.result) {
is CreateAuthRequestResult.Success -> { is CreateAuthRequestResult.Success -> {
updateContent { content ->
content.copy(isResendNotificationLoading = false)
}
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
dialogState = null, dialogState = null,
@ -123,7 +127,6 @@ class LoginWithDeviceViewModel @Inject constructor(
viewState = LoginWithDeviceState.ViewState.Content( viewState = LoginWithDeviceState.ViewState.Content(
loginWithDeviceType = it.loginWithDeviceType, loginWithDeviceType = it.loginWithDeviceType,
fingerprintPhrase = result.authRequest.fingerprint, fingerprintPhrase = result.authRequest.fingerprint,
isResendNotificationLoading = false,
), ),
dialogState = null, dialogState = null,
) )
@ -131,9 +134,6 @@ class LoginWithDeviceViewModel @Inject constructor(
} }
is CreateAuthRequestResult.Error -> { is CreateAuthRequestResult.Error -> {
updateContent { content ->
content.copy(isResendNotificationLoading = false)
}
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
dialogState = LoginWithDeviceState.DialogState.Error( dialogState = LoginWithDeviceState.DialogState.Error(
@ -149,9 +149,6 @@ class LoginWithDeviceViewModel @Inject constructor(
CreateAuthRequestResult.Declined -> Unit CreateAuthRequestResult.Declined -> Unit
CreateAuthRequestResult.Expired -> { CreateAuthRequestResult.Expired -> {
updateContent { content ->
content.copy(isResendNotificationLoading = false)
}
mutableStateFlow.update { mutableStateFlow.update {
it.copy( it.copy(
dialogState = LoginWithDeviceState.DialogState.Error( dialogState = LoginWithDeviceState.DialogState.Error(
@ -279,8 +276,7 @@ class LoginWithDeviceViewModel @Inject constructor(
} }
} }
private fun sendNewAuthRequest(isResend: Boolean) { private fun sendNewAuthRequest() {
setIsResendNotificationLoading(isResend)
authJob.cancel() authJob.cancel()
authJob = authRepository authJob = authRepository
.createAuthRequestWithUpdates( .createAuthRequestWithUpdates(
@ -291,22 +287,6 @@ class LoginWithDeviceViewModel @Inject constructor(
.onEach(::sendAction) .onEach(::sendAction)
.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
private fun setIsResendNotificationLoading(isResend: Boolean) {
updateContent { it.copy(isResendNotificationLoading = isResend) }
}
private inline fun updateContent(
crossinline block: (
LoginWithDeviceState.ViewState.Content,
) -> LoginWithDeviceState.ViewState.Content?,
) {
val currentViewState = state.viewState
val updatedContent = (currentViewState as? LoginWithDeviceState.ViewState.Content)
?.let(block)
?: return
mutableStateFlow.update { it.copy(viewState = updatedContent) }
}
} }
/** /**
@ -349,13 +329,10 @@ data class LoginWithDeviceState(
* Content state for the [LoginWithDeviceScreen] showing the actual content or items. * Content state for the [LoginWithDeviceScreen] showing the actual content or items.
* *
* @property fingerprintPhrase The fingerprint phrase to present to the user. * @property fingerprintPhrase The fingerprint phrase to present to the user.
* @property isResendNotificationLoading Indicates if the resend loading spinner should be
* displayed.
*/ */
@Parcelize @Parcelize
data class Content( data class Content(
val fingerprintPhrase: String, val fingerprintPhrase: String,
val isResendNotificationLoading: Boolean,
private val loginWithDeviceType: LoginWithDeviceType, private val loginWithDeviceType: LoginWithDeviceType,
) : ViewState() { ) : ViewState() {
/** /**
@ -401,14 +378,19 @@ data class LoginWithDeviceState(
/** /**
* The text to display indicating that there are other option for logging in. * The text to display indicating that there are other option for logging in.
*/ */
@Suppress("MaxLineLength")
val otherOptions: Text val otherOptions: Text
get() = when (loginWithDeviceType) { get() = when (loginWithDeviceType) {
LoginWithDeviceType.OTHER_DEVICE, LoginWithDeviceType.OTHER_DEVICE,
LoginWithDeviceType.SSO_OTHER_DEVICE, LoginWithDeviceType.SSO_OTHER_DEVICE,
-> BitwardenString.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option.asText() -> {
BitwardenString
.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app
.asText()
}
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> BitwardenString.trouble_logging_in.asText() LoginWithDeviceType.SSO_ADMIN_APPROVAL -> {
BitwardenString.trouble_logging_in.asText()
}
} }
/** /**

View File

@ -7,6 +7,7 @@ import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performFirstLinkClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.IntentManager import com.bitwarden.ui.platform.manager.IntentManager
@ -92,7 +93,10 @@ class LoginWithDeviceScreenTest : BitwardenComposeTest() {
@Test @Test
fun `view all log in options click should send ViewAllLogInOptionsClick action`() { fun `view all log in options click should send ViewAllLogInOptionsClick action`() {
composeTestRule.onNodeWithText("View all log in options").performScrollTo().performClick() composeTestRule
.onNodeWithText(text = "Need another option? View all login options")
.performScrollTo()
.performFirstLinkClick()
verify { verify {
viewModel.trySendAction(LoginWithDeviceAction.ViewAllLogInOptionsClick) viewModel.trySendAction(LoginWithDeviceAction.ViewAllLogInOptionsClick)
} }
@ -168,7 +172,6 @@ private val DEFAULT_STATE = LoginWithDeviceState(
emailAddress = EMAIL, emailAddress = EMAIL,
viewState = LoginWithDeviceState.ViewState.Content( viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate", fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
isResendNotificationLoading = false,
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE, loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
), ),
dialogState = null, dialogState = null,

View File

@ -121,8 +121,8 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(LoginWithDeviceAction.ResendNotificationClick) viewModel.trySendAction(LoginWithDeviceAction.ResendNotificationClick)
assertEquals( assertEquals(
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy( dialogState = LoginWithDeviceState.DialogState.Loading(
isResendNotificationLoading = true, message = BitwardenString.resending.asText(),
), ),
), ),
viewModel.stateFlow.value, viewModel.stateFlow.value,
@ -610,7 +610,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy( viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
fingerprintPhrase = FINGERPRINT, fingerprintPhrase = FINGERPRINT,
isResendNotificationLoading = false,
), ),
dialogState = LoginWithDeviceState.DialogState.Error( dialogState = LoginWithDeviceState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(), title = BitwardenString.an_error_has_occurred.asText(),
@ -661,7 +660,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
DEFAULT_STATE.copy( DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy( viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
fingerprintPhrase = FINGERPRINT, fingerprintPhrase = FINGERPRINT,
isResendNotificationLoading = false,
), ),
dialogState = LoginWithDeviceState.DialogState.Error( dialogState = LoginWithDeviceState.DialogState.Error(
title = null, title = null,
@ -693,7 +691,6 @@ private const val FINGERPRINT = "fingerprint"
private val DEFAULT_CONTENT_VIEW_STATE = LoginWithDeviceState.ViewState.Content( private val DEFAULT_CONTENT_VIEW_STATE = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = FINGERPRINT, fingerprintPhrase = FINGERPRINT,
isResendNotificationLoading = false,
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE, loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
) )

View File

@ -37,6 +37,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
@ -94,6 +95,7 @@ import kotlinx.collections.immutable.toImmutableList
* @param readOnly `true` if the input should be read-only and not accept user interactions. * @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param enabled Whether or not the text field is enabled. * @param enabled Whether or not the text field is enabled.
* @param textStyle An optional style that may be used to override the default used. * @param textStyle An optional style that may be used to override the default used.
* @param textColor An optional color that may be used to override the text color.
* @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling * @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling
* an entire line before breaking. `false` by default. * an entire line before breaking. `false` by default.
* @param visualTransformation Transforms the visual representation of the input [value]. * @param visualTransformation Transforms the visual representation of the input [value].
@ -123,6 +125,7 @@ fun BitwardenTextField(
readOnly: Boolean = false, readOnly: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
textStyle: TextStyle = BitwardenTheme.typography.bodyLarge, textStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
textColor: Color = BitwardenTheme.colorScheme.text.primary,
shouldAddCustomLineBreaks: Boolean = false, shouldAddCustomLineBreaks: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text, keyboardType: KeyboardType = KeyboardType.Text,
keyboardActions: KeyboardActions = KeyboardActions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default,
@ -158,6 +161,7 @@ fun BitwardenTextField(
readOnly = readOnly, readOnly = readOnly,
enabled = enabled, enabled = enabled,
textStyle = textStyle, textStyle = textStyle,
textColor = textColor,
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks, shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
keyboardType = keyboardType, keyboardType = keyboardType,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
@ -194,6 +198,7 @@ fun BitwardenTextField(
* @param readOnly `true` if the input should be read-only and not accept user interactions. * @param readOnly `true` if the input should be read-only and not accept user interactions.
* @param enabled Whether or not the text field is enabled. * @param enabled Whether or not the text field is enabled.
* @param textStyle An optional style that may be used to override the default used. * @param textStyle An optional style that may be used to override the default used.
* @param textColor An optional color that may be used to override the text color.
* @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling * @param shouldAddCustomLineBreaks If `true`, line breaks will be inserted to allow for filling
* an entire line before breaking. `false` by default. * an entire line before breaking. `false` by default.
* @param visualTransformation Transforms the visual representation of the input [value]. * @param visualTransformation Transforms the visual representation of the input [value].
@ -226,6 +231,7 @@ fun BitwardenTextField(
readOnly: Boolean = false, readOnly: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
textStyle: TextStyle = BitwardenTheme.typography.bodyLarge, textStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
textColor: Color = BitwardenTheme.colorScheme.text.primary,
shouldAddCustomLineBreaks: Boolean = false, shouldAddCustomLineBreaks: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text, keyboardType: KeyboardType = KeyboardType.Text,
keyboardActions: KeyboardActions = KeyboardActions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default,
@ -312,7 +318,7 @@ fun BitwardenTextField(
var focused by remember { mutableStateOf(false) } var focused by remember { mutableStateOf(false) }
TextField( TextField(
colors = bitwardenTextFieldColors(), colors = bitwardenTextFieldColors(textColor = textColor),
enabled = enabled, enabled = enabled,
label = label?.let { label = label?.let {
{ {

View File

@ -24,6 +24,7 @@ fun bitwardenTextFieldButtonColors(): TextFieldColors = bitwardenTextFieldColors
*/ */
@Composable @Composable
fun bitwardenTextFieldColors( fun bitwardenTextFieldColors(
textColor: Color = BitwardenTheme.colorScheme.text.primary,
disabledTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled, disabledTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledLeadingIconColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled, disabledLeadingIconColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledTrailingIconColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled, disabledTrailingIconColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
@ -31,8 +32,8 @@ fun bitwardenTextFieldColors(
disabledPlaceholderColor: Color = BitwardenTheme.colorScheme.text.secondary, disabledPlaceholderColor: Color = BitwardenTheme.colorScheme.text.secondary,
disabledSupportingTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled, disabledSupportingTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
): TextFieldColors = TextFieldColors( ): TextFieldColors = TextFieldColors(
focusedTextColor = BitwardenTheme.colorScheme.text.primary, focusedTextColor = textColor,
unfocusedTextColor = BitwardenTheme.colorScheme.text.primary, unfocusedTextColor = textColor,
disabledTextColor = disabledTextColor, disabledTextColor = disabledTextColor,
errorTextColor = BitwardenTheme.colorScheme.text.primary, errorTextColor = BitwardenTheme.colorScheme.text.primary,
focusedContainerColor = Color.Transparent, focusedContainerColor = Color.Transparent,

View File

@ -653,7 +653,8 @@ Do you want to switch to this account?</string>
<string name="invalid_uri">Invalid URI</string> <string name="invalid_uri">Invalid URI</string>
<string name="the_urix_is_already_blocked">The URI %1$s is already blocked</string> <string name="the_urix_is_already_blocked">The URI %1$s is already blocked</string>
<string name="login_approved">Login approved</string> <string name="login_approved">Login approved</string>
<string name="log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option">Log in with device must be set up in the settings of the Bitwarden app. Need another option?</string> <string name="log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app">Log in with device must be set up in the settings of the Bitwarden app.</string>
<string name="need_another_option_view_all_login_options">Need another option? <annotation link="viewAll">View all login options</annotation></string>
<string name="log_in_with_device">Log in with device</string> <string name="log_in_with_device">Log in with device</string>
<string name="logging_in_on">Logging in on</string> <string name="logging_in_on">Logging in on</string>
<string name="logging_in_on_with_colon">Logging in on:</string> <string name="logging_in_on_with_colon">Logging in on:</string>
@ -1156,4 +1157,5 @@ Do you want to switch to this account?</string>
<string name="lock_app">Lock app</string> <string name="lock_app">Lock app</string>
<string name="use_your_devices_lock_method_to_unlock_the_app">Use your devices lock method to unlock the app</string> <string name="use_your_devices_lock_method_to_unlock_the_app">Use your devices lock method to unlock the app</string>
<string name="loading_vault_data">Loading vault data…</string> <string name="loading_vault_data">Loading vault data…</string>
<string name="resending">Resending</string>
</resources> </resources>