diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt
index 1b2c30e0fb..368126de3d 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt
@@ -1,16 +1,11 @@
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.PaddingValues
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.defaultMinSize
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.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -20,7 +15,6 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
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.lifecycle.compose.collectAsStateWithLifecycle
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.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
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.text.BitwardenClickableText
+import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
@@ -120,111 +118,99 @@ private fun LoginWithDeviceScreenContent(
modifier = modifier
.verticalScroll(rememberScrollState()),
) {
+ Spacer(modifier = Modifier.height(height = 24.dp))
+
Text(
text = state.title(),
- textAlign = TextAlign.Start,
- style = BitwardenTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center,
+ style = BitwardenTheme.typography.titleMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
- .padding(horizontal = 16.dp)
+ .standardHorizontalMargin()
.fillMaxWidth(),
)
- Spacer(modifier = Modifier.height(24.dp))
+ Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = state.subtitle(),
- textAlign = TextAlign.Start,
+ textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
- .padding(horizontal = 16.dp)
+ .standardHorizontalMargin()
.fillMaxWidth(),
)
- Spacer(modifier = Modifier.height(16.dp))
+ Spacer(modifier = Modifier.height(height = 12.dp))
Text(
text = state.description(),
- textAlign = TextAlign.Start,
+ textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium,
color = BitwardenTheme.colorScheme.text.primary,
modifier = Modifier
- .padding(horizontal = 16.dp)
+ .standardHorizontalMargin()
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(24.dp))
- Text(
- text = stringResource(id = BitwardenString.fingerprint_phrase),
- textAlign = TextAlign.Start,
- style = BitwardenTheme.typography.titleLarge,
- color = BitwardenTheme.colorScheme.text.primary,
+ BitwardenTextField(
+ label = stringResource(id = BitwardenString.fingerprint_phrase),
+ value = state.fingerprintPhrase,
+ textFieldTestTag = "FingerprintPhraseValue",
+ onValueChange = { },
+ readOnly = true,
+ singleLine = false,
+ textToolbarType = TextToolbarType.NONE,
+ textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
+ textColor = BitwardenTheme.colorScheme.text.codePink,
+ cardStyle = CardStyle.Full,
modifier = Modifier
- .padding(horizontal = 16.dp)
- .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)
+ .standardHorizontalMargin()
.fillMaxWidth(),
)
if (state.allowsResend) {
- Column(
- verticalArrangement = Arrangement.Center,
+ Spacer(modifier = Modifier.height(height = 24.dp))
+ BitwardenOutlinedButton(
+ label = stringResource(id = BitwardenString.resend_notification),
+ onClick = onResendNotificationClick,
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),
- style = BitwardenTheme.typography.labelLarge,
- innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
- onClick = onResendNotificationClick,
- )
- }
- }
+ .testTag(tag = "ResendNotificationButton")
+ .standardHorizontalMargin()
+ .fillMaxWidth(),
+ )
}
- Spacer(modifier = Modifier.height(28.dp))
+ Spacer(modifier = Modifier.height(height = 24.dp))
Text(
text = state.otherOptions(),
- textAlign = TextAlign.Start,
- style = BitwardenTheme.typography.bodyMedium,
- color = BitwardenTheme.colorScheme.text.primary,
+ textAlign = TextAlign.Center,
+ style = BitwardenTheme.typography.bodySmall,
+ color = BitwardenTheme.colorScheme.text.secondary,
modifier = Modifier
- .padding(horizontal = 16.dp)
+ .standardHorizontalMargin()
.fillMaxWidth(),
)
- BitwardenClickableText(
- modifier = Modifier.testTag("ViewAllLoginOptionsButton"),
- label = stringResource(id = BitwardenString.view_all_login_options),
- innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
- style = BitwardenTheme.typography.labelLarge,
+ Spacer(modifier = Modifier.height(height = 12.dp))
+
+ BitwardenHyperTextLink(
+ annotatedResId = BitwardenString.need_another_option_view_all_login_options,
+ annotationKey = "viewAll",
+ accessibilityString = stringResource(id = BitwardenString.view_all_login_options),
onClick = onViewAllLogInOptionsClick,
+ style = BitwardenTheme.typography.bodySmall,
+ modifier = Modifier
+ .testTag(tag = "ViewAllLoginOptionsButton")
+ .standardHorizontalMargin()
+ .fillMaxWidth(),
)
+ Spacer(modifier = Modifier.height(height = 12.dp))
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt
index 5bca77ce3b..4ed386b42b 100644
--- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt
+++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt
@@ -52,7 +52,7 @@ class LoginWithDeviceViewModel @Inject constructor(
private var authJob: Job = Job().apply { complete() }
init {
- sendNewAuthRequest(isResend = false)
+ sendNewAuthRequest()
}
override fun handleAction(action: LoginWithDeviceAction) {
@@ -74,7 +74,14 @@ class LoginWithDeviceViewModel @Inject constructor(
}
private fun handleResendNotificationClicked() {
- sendNewAuthRequest(isResend = true)
+ mutableStateFlow.update {
+ it.copy(
+ dialogState = LoginWithDeviceState.DialogState.Loading(
+ message = BitwardenString.resending.asText(),
+ ),
+ )
+ }
+ sendNewAuthRequest()
}
private fun handleViewAllLogInOptionsClicked() {
@@ -99,9 +106,6 @@ class LoginWithDeviceViewModel @Inject constructor(
) {
when (val result = action.result) {
is CreateAuthRequestResult.Success -> {
- updateContent { content ->
- content.copy(isResendNotificationLoading = false)
- }
mutableStateFlow.update {
it.copy(
dialogState = null,
@@ -123,7 +127,6 @@ class LoginWithDeviceViewModel @Inject constructor(
viewState = LoginWithDeviceState.ViewState.Content(
loginWithDeviceType = it.loginWithDeviceType,
fingerprintPhrase = result.authRequest.fingerprint,
- isResendNotificationLoading = false,
),
dialogState = null,
)
@@ -131,9 +134,6 @@ class LoginWithDeviceViewModel @Inject constructor(
}
is CreateAuthRequestResult.Error -> {
- updateContent { content ->
- content.copy(isResendNotificationLoading = false)
- }
mutableStateFlow.update {
it.copy(
dialogState = LoginWithDeviceState.DialogState.Error(
@@ -149,9 +149,6 @@ class LoginWithDeviceViewModel @Inject constructor(
CreateAuthRequestResult.Declined -> Unit
CreateAuthRequestResult.Expired -> {
- updateContent { content ->
- content.copy(isResendNotificationLoading = false)
- }
mutableStateFlow.update {
it.copy(
dialogState = LoginWithDeviceState.DialogState.Error(
@@ -279,8 +276,7 @@ class LoginWithDeviceViewModel @Inject constructor(
}
}
- private fun sendNewAuthRequest(isResend: Boolean) {
- setIsResendNotificationLoading(isResend)
+ private fun sendNewAuthRequest() {
authJob.cancel()
authJob = authRepository
.createAuthRequestWithUpdates(
@@ -291,22 +287,6 @@ class LoginWithDeviceViewModel @Inject constructor(
.onEach(::sendAction)
.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.
*
* @property fingerprintPhrase The fingerprint phrase to present to the user.
- * @property isResendNotificationLoading Indicates if the resend loading spinner should be
- * displayed.
*/
@Parcelize
data class Content(
val fingerprintPhrase: String,
- val isResendNotificationLoading: Boolean,
private val loginWithDeviceType: LoginWithDeviceType,
) : ViewState() {
/**
@@ -401,14 +378,19 @@ data class LoginWithDeviceState(
/**
* The text to display indicating that there are other option for logging in.
*/
- @Suppress("MaxLineLength")
val otherOptions: Text
get() = when (loginWithDeviceType) {
LoginWithDeviceType.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()
+ }
}
/**
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt
index d775790033..14f4c01895 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt
@@ -7,6 +7,7 @@ import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performFirstLinkClick
import androidx.compose.ui.test.performScrollTo
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.ui.platform.manager.IntentManager
@@ -92,7 +93,10 @@ class LoginWithDeviceScreenTest : BitwardenComposeTest() {
@Test
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 {
viewModel.trySendAction(LoginWithDeviceAction.ViewAllLogInOptionsClick)
}
@@ -168,7 +172,6 @@ private val DEFAULT_STATE = LoginWithDeviceState(
emailAddress = EMAIL,
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
- isResendNotificationLoading = false,
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
),
dialogState = null,
diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt
index b42aee2eeb..f51b3dd711 100644
--- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt
+++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt
@@ -121,8 +121,8 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewModel.trySendAction(LoginWithDeviceAction.ResendNotificationClick)
assertEquals(
DEFAULT_STATE.copy(
- viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
- isResendNotificationLoading = true,
+ dialogState = LoginWithDeviceState.DialogState.Loading(
+ message = BitwardenString.resending.asText(),
),
),
viewModel.stateFlow.value,
@@ -610,7 +610,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
fingerprintPhrase = FINGERPRINT,
- isResendNotificationLoading = false,
),
dialogState = LoginWithDeviceState.DialogState.Error(
title = BitwardenString.an_error_has_occurred.asText(),
@@ -661,7 +660,6 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
DEFAULT_STATE.copy(
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
fingerprintPhrase = FINGERPRINT,
- isResendNotificationLoading = false,
),
dialogState = LoginWithDeviceState.DialogState.Error(
title = null,
@@ -693,7 +691,6 @@ private const val FINGERPRINT = "fingerprint"
private val DEFAULT_CONTENT_VIEW_STATE = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = FINGERPRINT,
- isResendNotificationLoading = false,
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
)
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt
index 7abeab85a5..1f353bee02 100644
--- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/BitwardenTextField.kt
@@ -37,6 +37,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.focus.onFocusEvent
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalClipboard
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 enabled Whether or not the text field is enabled.
* @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
* an entire line before breaking. `false` by default.
* @param visualTransformation Transforms the visual representation of the input [value].
@@ -123,6 +125,7 @@ fun BitwardenTextField(
readOnly: Boolean = false,
enabled: Boolean = true,
textStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
+ textColor: Color = BitwardenTheme.colorScheme.text.primary,
shouldAddCustomLineBreaks: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text,
keyboardActions: KeyboardActions = KeyboardActions.Default,
@@ -158,6 +161,7 @@ fun BitwardenTextField(
readOnly = readOnly,
enabled = enabled,
textStyle = textStyle,
+ textColor = textColor,
shouldAddCustomLineBreaks = shouldAddCustomLineBreaks,
keyboardType = keyboardType,
keyboardActions = keyboardActions,
@@ -194,6 +198,7 @@ fun BitwardenTextField(
* @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 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
* an entire line before breaking. `false` by default.
* @param visualTransformation Transforms the visual representation of the input [value].
@@ -226,6 +231,7 @@ fun BitwardenTextField(
readOnly: Boolean = false,
enabled: Boolean = true,
textStyle: TextStyle = BitwardenTheme.typography.bodyLarge,
+ textColor: Color = BitwardenTheme.colorScheme.text.primary,
shouldAddCustomLineBreaks: Boolean = false,
keyboardType: KeyboardType = KeyboardType.Text,
keyboardActions: KeyboardActions = KeyboardActions.Default,
@@ -312,7 +318,7 @@ fun BitwardenTextField(
var focused by remember { mutableStateOf(false) }
TextField(
- colors = bitwardenTextFieldColors(),
+ colors = bitwardenTextFieldColors(textColor = textColor),
enabled = enabled,
label = label?.let {
{
diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt
index 8e1066aabc..856827b3e9 100644
--- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt
+++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt
@@ -24,6 +24,7 @@ fun bitwardenTextFieldButtonColors(): TextFieldColors = bitwardenTextFieldColors
*/
@Composable
fun bitwardenTextFieldColors(
+ textColor: Color = BitwardenTheme.colorScheme.text.primary,
disabledTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledLeadingIconColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledTrailingIconColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
@@ -31,8 +32,8 @@ fun bitwardenTextFieldColors(
disabledPlaceholderColor: Color = BitwardenTheme.colorScheme.text.secondary,
disabledSupportingTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
): TextFieldColors = TextFieldColors(
- focusedTextColor = BitwardenTheme.colorScheme.text.primary,
- unfocusedTextColor = BitwardenTheme.colorScheme.text.primary,
+ focusedTextColor = textColor,
+ unfocusedTextColor = textColor,
disabledTextColor = disabledTextColor,
errorTextColor = BitwardenTheme.colorScheme.text.primary,
focusedContainerColor = Color.Transparent,
diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml
index 8a371f3323..8f61d161cd 100644
--- a/ui/src/main/res/values/strings.xml
+++ b/ui/src/main/res/values/strings.xml
@@ -653,7 +653,8 @@ Do you want to switch to this account?
Invalid URI
The URI %1$s is already blocked
Login approved
- 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? View all login options
Log in with device
Logging in on
Logging in on:
@@ -1156,4 +1157,5 @@ Do you want to switch to this account?
Lock app
Use your device’s lock method to unlock the app
Loading vault data…
+ Resending