mirror of
https://github.com/bitwarden/android.git
synced 2026-03-11 20:54:58 -05:00
PM-28522: Update the Login With Device Screen (#6184)
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user