Skip to content

Commit f519524

Browse files
PM-28522: Update the Login With Device Screen
1 parent f02b374 commit f519524

File tree

7 files changed

+93
-116
lines changed

7 files changed

+93
-116
lines changed

app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt

Lines changed: 54 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
22

3-
import androidx.compose.foundation.layout.Arrangement
43
import androidx.compose.foundation.layout.Column
5-
import androidx.compose.foundation.layout.PaddingValues
64
import androidx.compose.foundation.layout.Spacer
7-
import androidx.compose.foundation.layout.defaultMinSize
85
import androidx.compose.foundation.layout.fillMaxSize
96
import androidx.compose.foundation.layout.fillMaxWidth
107
import androidx.compose.foundation.layout.height
118
import androidx.compose.foundation.layout.navigationBarsPadding
12-
import androidx.compose.foundation.layout.padding
13-
import androidx.compose.foundation.layout.size
149
import androidx.compose.foundation.rememberScrollState
1510
import androidx.compose.foundation.verticalScroll
1611
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -20,7 +15,6 @@ import androidx.compose.material3.rememberTopAppBarState
2015
import androidx.compose.runtime.Composable
2116
import androidx.compose.runtime.getValue
2217
import androidx.compose.runtime.remember
23-
import androidx.compose.ui.Alignment
2418
import androidx.compose.ui.Modifier
2519
import androidx.compose.ui.input.nestedscroll.nestedScroll
2620
import androidx.compose.ui.platform.testTag
@@ -30,13 +24,17 @@ import androidx.compose.ui.unit.dp
3024
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
3125
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3226
import com.bitwarden.ui.platform.base.util.EventsEffect
27+
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
3328
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
29+
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
3430
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
3531
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
3632
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
37-
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularProgressIndicator
33+
import com.bitwarden.ui.platform.components.field.BitwardenTextField
34+
import com.bitwarden.ui.platform.components.field.model.TextToolbarType
35+
import com.bitwarden.ui.platform.components.model.CardStyle
3836
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
39-
import com.bitwarden.ui.platform.components.text.BitwardenClickableText
37+
import com.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
4038
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
4139
import com.bitwarden.ui.platform.resource.BitwardenDrawable
4240
import com.bitwarden.ui.platform.resource.BitwardenString
@@ -120,111 +118,99 @@ private fun LoginWithDeviceScreenContent(
120118
modifier = modifier
121119
.verticalScroll(rememberScrollState()),
122120
) {
121+
Spacer(modifier = Modifier.height(height = 24.dp))
122+
123123
Text(
124124
text = state.title(),
125-
textAlign = TextAlign.Start,
126-
style = BitwardenTheme.typography.headlineMedium,
125+
textAlign = TextAlign.Center,
126+
style = BitwardenTheme.typography.titleMedium,
127127
color = BitwardenTheme.colorScheme.text.primary,
128128
modifier = Modifier
129-
.padding(horizontal = 16.dp)
129+
.standardHorizontalMargin()
130130
.fillMaxWidth(),
131131
)
132132

133-
Spacer(modifier = Modifier.height(24.dp))
133+
Spacer(modifier = Modifier.height(height = 12.dp))
134134

135135
Text(
136136
text = state.subtitle(),
137-
textAlign = TextAlign.Start,
137+
textAlign = TextAlign.Center,
138138
style = BitwardenTheme.typography.bodyMedium,
139139
color = BitwardenTheme.colorScheme.text.primary,
140140
modifier = Modifier
141-
.padding(horizontal = 16.dp)
141+
.standardHorizontalMargin()
142142
.fillMaxWidth(),
143143
)
144144

145-
Spacer(modifier = Modifier.height(16.dp))
145+
Spacer(modifier = Modifier.height(height = 12.dp))
146146

147147
Text(
148148
text = state.description(),
149-
textAlign = TextAlign.Start,
149+
textAlign = TextAlign.Center,
150150
style = BitwardenTheme.typography.bodyMedium,
151151
color = BitwardenTheme.colorScheme.text.primary,
152152
modifier = Modifier
153-
.padding(horizontal = 16.dp)
153+
.standardHorizontalMargin()
154154
.fillMaxWidth(),
155155
)
156156

157157
Spacer(modifier = Modifier.height(24.dp))
158158

159-
Text(
160-
text = stringResource(id = BitwardenString.fingerprint_phrase),
161-
textAlign = TextAlign.Start,
162-
style = BitwardenTheme.typography.titleLarge,
163-
color = BitwardenTheme.colorScheme.text.primary,
159+
BitwardenTextField(
160+
label = stringResource(id = BitwardenString.fingerprint_phrase),
161+
value = state.fingerprintPhrase,
162+
textFieldTestTag = "FingerprintPhraseValue",
163+
onValueChange = { },
164+
readOnly = true,
165+
singleLine = false,
166+
textToolbarType = TextToolbarType.NONE,
167+
textStyle = BitwardenTheme.typography.sensitiveInfoSmall,
168+
textColor = BitwardenTheme.colorScheme.text.codePink,
169+
cardStyle = CardStyle.Full,
164170
modifier = Modifier
165-
.padding(horizontal = 16.dp)
166-
.fillMaxWidth(),
167-
)
168-
169-
Spacer(modifier = Modifier.height(12.dp))
170-
171-
Text(
172-
text = state.fingerprintPhrase,
173-
textAlign = TextAlign.Start,
174-
color = BitwardenTheme.colorScheme.text.codePink,
175-
style = BitwardenTheme.typography.sensitiveInfoSmall,
176-
minLines = 2,
177-
modifier = Modifier
178-
.testTag("FingerprintPhraseValue")
179-
.padding(horizontal = 16.dp)
171+
.standardHorizontalMargin()
180172
.fillMaxWidth(),
181173
)
182174

183175
if (state.allowsResend) {
184-
Column(
185-
verticalArrangement = Arrangement.Center,
176+
Spacer(modifier = Modifier.height(height = 24.dp))
177+
BitwardenOutlinedButton(
178+
label = stringResource(id = BitwardenString.resend_notification),
179+
onClick = onResendNotificationClick,
186180
modifier = Modifier
187-
.defaultMinSize(minHeight = 40.dp)
188-
.align(Alignment.Start),
189-
) {
190-
if (state.isResendNotificationLoading) {
191-
BitwardenCircularProgressIndicator(
192-
modifier = Modifier
193-
.padding(horizontal = 64.dp)
194-
.size(size = 16.dp),
195-
)
196-
} else {
197-
BitwardenClickableText(
198-
modifier = Modifier.testTag("ResendNotificationButton"),
199-
label = stringResource(id = BitwardenString.resend_notification),
200-
style = BitwardenTheme.typography.labelLarge,
201-
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
202-
onClick = onResendNotificationClick,
203-
)
204-
}
205-
}
181+
.testTag(tag = "ResendNotificationButton")
182+
.standardHorizontalMargin()
183+
.fillMaxWidth(),
184+
)
206185
}
207186

208-
Spacer(modifier = Modifier.height(28.dp))
187+
Spacer(modifier = Modifier.height(height = 24.dp))
209188

210189
Text(
211190
text = state.otherOptions(),
212-
textAlign = TextAlign.Start,
213-
style = BitwardenTheme.typography.bodyMedium,
214-
color = BitwardenTheme.colorScheme.text.primary,
191+
textAlign = TextAlign.Center,
192+
style = BitwardenTheme.typography.bodySmall,
193+
color = BitwardenTheme.colorScheme.text.secondary,
215194
modifier = Modifier
216-
.padding(horizontal = 16.dp)
195+
.standardHorizontalMargin()
217196
.fillMaxWidth(),
218197
)
219198

220-
BitwardenClickableText(
221-
modifier = Modifier.testTag("ViewAllLoginOptionsButton"),
222-
label = stringResource(id = BitwardenString.view_all_login_options),
223-
innerPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp),
224-
style = BitwardenTheme.typography.labelLarge,
199+
Spacer(modifier = Modifier.height(height = 12.dp))
200+
201+
BitwardenHyperTextLink(
202+
annotatedResId = BitwardenString.need_another_option_view_all_login_options,
203+
annotationKey = "viewAll",
204+
accessibilityString = stringResource(id = BitwardenString.view_all_login_options),
225205
onClick = onViewAllLogInOptionsClick,
206+
style = BitwardenTheme.typography.bodySmall,
207+
modifier = Modifier
208+
.testTag(tag = "ViewAllLoginOptionsButton")
209+
.standardHorizontalMargin()
210+
.fillMaxWidth(),
226211
)
227212

213+
Spacer(modifier = Modifier.height(height = 12.dp))
228214
Spacer(modifier = Modifier.navigationBarsPadding())
229215
}
230216
}

app/src/main/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class LoginWithDeviceViewModel @Inject constructor(
5252
private var authJob: Job = Job().apply { complete() }
5353

5454
init {
55-
sendNewAuthRequest(isResend = false)
55+
sendNewAuthRequest()
5656
}
5757

5858
override fun handleAction(action: LoginWithDeviceAction) {
@@ -74,7 +74,14 @@ class LoginWithDeviceViewModel @Inject constructor(
7474
}
7575

7676
private fun handleResendNotificationClicked() {
77-
sendNewAuthRequest(isResend = true)
77+
mutableStateFlow.update {
78+
it.copy(
79+
dialogState = LoginWithDeviceState.DialogState.Loading(
80+
message = BitwardenString.resending.asText(),
81+
),
82+
)
83+
}
84+
sendNewAuthRequest()
7885
}
7986

8087
private fun handleViewAllLogInOptionsClicked() {
@@ -99,9 +106,6 @@ class LoginWithDeviceViewModel @Inject constructor(
99106
) {
100107
when (val result = action.result) {
101108
is CreateAuthRequestResult.Success -> {
102-
updateContent { content ->
103-
content.copy(isResendNotificationLoading = false)
104-
}
105109
mutableStateFlow.update {
106110
it.copy(
107111
dialogState = null,
@@ -123,17 +127,13 @@ class LoginWithDeviceViewModel @Inject constructor(
123127
viewState = LoginWithDeviceState.ViewState.Content(
124128
loginWithDeviceType = it.loginWithDeviceType,
125129
fingerprintPhrase = result.authRequest.fingerprint,
126-
isResendNotificationLoading = false,
127130
),
128131
dialogState = null,
129132
)
130133
}
131134
}
132135

133136
is CreateAuthRequestResult.Error -> {
134-
updateContent { content ->
135-
content.copy(isResendNotificationLoading = false)
136-
}
137137
mutableStateFlow.update {
138138
it.copy(
139139
dialogState = LoginWithDeviceState.DialogState.Error(
@@ -149,9 +149,6 @@ class LoginWithDeviceViewModel @Inject constructor(
149149
CreateAuthRequestResult.Declined -> Unit
150150

151151
CreateAuthRequestResult.Expired -> {
152-
updateContent { content ->
153-
content.copy(isResendNotificationLoading = false)
154-
}
155152
mutableStateFlow.update {
156153
it.copy(
157154
dialogState = LoginWithDeviceState.DialogState.Error(
@@ -279,8 +276,7 @@ class LoginWithDeviceViewModel @Inject constructor(
279276
}
280277
}
281278

282-
private fun sendNewAuthRequest(isResend: Boolean) {
283-
setIsResendNotificationLoading(isResend)
279+
private fun sendNewAuthRequest() {
284280
authJob.cancel()
285281
authJob = authRepository
286282
.createAuthRequestWithUpdates(
@@ -291,22 +287,6 @@ class LoginWithDeviceViewModel @Inject constructor(
291287
.onEach(::sendAction)
292288
.launchIn(viewModelScope)
293289
}
294-
295-
private fun setIsResendNotificationLoading(isResend: Boolean) {
296-
updateContent { it.copy(isResendNotificationLoading = isResend) }
297-
}
298-
299-
private inline fun updateContent(
300-
crossinline block: (
301-
LoginWithDeviceState.ViewState.Content,
302-
) -> LoginWithDeviceState.ViewState.Content?,
303-
) {
304-
val currentViewState = state.viewState
305-
val updatedContent = (currentViewState as? LoginWithDeviceState.ViewState.Content)
306-
?.let(block)
307-
?: return
308-
mutableStateFlow.update { it.copy(viewState = updatedContent) }
309-
}
310290
}
311291

312292
/**
@@ -349,13 +329,10 @@ data class LoginWithDeviceState(
349329
* Content state for the [LoginWithDeviceScreen] showing the actual content or items.
350330
*
351331
* @property fingerprintPhrase The fingerprint phrase to present to the user.
352-
* @property isResendNotificationLoading Indicates if the resend loading spinner should be
353-
* displayed.
354332
*/
355333
@Parcelize
356334
data class Content(
357335
val fingerprintPhrase: String,
358-
val isResendNotificationLoading: Boolean,
359336
private val loginWithDeviceType: LoginWithDeviceType,
360337
) : ViewState() {
361338
/**
@@ -401,14 +378,19 @@ data class LoginWithDeviceState(
401378
/**
402379
* The text to display indicating that there are other option for logging in.
403380
*/
404-
@Suppress("MaxLineLength")
405381
val otherOptions: Text
406382
get() = when (loginWithDeviceType) {
407383
LoginWithDeviceType.OTHER_DEVICE,
408384
LoginWithDeviceType.SSO_OTHER_DEVICE,
409-
-> BitwardenString.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app_need_another_option.asText()
410-
411-
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> BitwardenString.trouble_logging_in.asText()
385+
-> {
386+
BitwardenString
387+
.log_in_with_device_must_be_set_up_in_the_settings_of_the_bitwarden_app
388+
.asText()
389+
}
390+
391+
LoginWithDeviceType.SSO_ADMIN_APPROVAL -> {
392+
BitwardenString.trouble_logging_in.asText()
393+
}
412394
}
413395

414396
/**

app/src/test/kotlin/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.ui.test.isDialog
77
import androidx.compose.ui.test.onNodeWithContentDescription
88
import androidx.compose.ui.test.onNodeWithText
99
import androidx.compose.ui.test.performClick
10+
import androidx.compose.ui.test.performFirstLinkClick
1011
import androidx.compose.ui.test.performScrollTo
1112
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
1213
import com.bitwarden.ui.platform.manager.IntentManager
@@ -92,7 +93,10 @@ class LoginWithDeviceScreenTest : BitwardenComposeTest() {
9293

9394
@Test
9495
fun `view all log in options click should send ViewAllLogInOptionsClick action`() {
95-
composeTestRule.onNodeWithText("View all log in options").performScrollTo().performClick()
96+
composeTestRule
97+
.onNodeWithText(text = "Need another option? View all login options")
98+
.performScrollTo()
99+
.performFirstLinkClick()
96100
verify {
97101
viewModel.trySendAction(LoginWithDeviceAction.ViewAllLogInOptionsClick)
98102
}
@@ -168,7 +172,6 @@ private val DEFAULT_STATE = LoginWithDeviceState(
168172
emailAddress = EMAIL,
169173
viewState = LoginWithDeviceState.ViewState.Content(
170174
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
171-
isResendNotificationLoading = false,
172175
loginWithDeviceType = LoginWithDeviceType.OTHER_DEVICE,
173176
),
174177
dialogState = null,

0 commit comments

Comments
 (0)