BIT-1563: Handle POST auth-requests error on Login with Device (#828)

This commit is contained in:
Caleb Derosier 2024-01-28 09:34:51 -07:00 committed by Álison Fernandes
parent ab0cfdfdc2
commit fa551fa6ab
4 changed files with 79 additions and 56 deletions

View file

@ -39,8 +39,10 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
@ -91,6 +93,9 @@ fun LoginWithDeviceScreen(
is LoginWithDeviceState.ViewState.Content -> {
LoginWithDeviceScreenContent(
state = viewState,
onErrorDialogDismiss = remember(viewModel) {
{ viewModel.trySendAction(LoginWithDeviceAction.ErrorDialogDismiss) }
},
onResendNotificationClick = remember(viewModel) {
{ viewModel.trySendAction(LoginWithDeviceAction.ResendNotificationClick) }
},
@ -101,13 +106,6 @@ fun LoginWithDeviceScreen(
)
}
is LoginWithDeviceState.ViewState.Error -> {
BitwardenErrorContent(
message = viewState.message(),
modifier = modifier,
)
}
LoginWithDeviceState.ViewState.Loading -> {
Column(
modifier = modifier,
@ -127,10 +125,23 @@ fun LoginWithDeviceScreen(
@Composable
private fun LoginWithDeviceScreenContent(
state: LoginWithDeviceState.ViewState.Content,
onErrorDialogDismiss: () -> Unit,
onResendNotificationClick: () -> Unit,
onViewAllLogInOptionsClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BitwardenBasicDialog(
visibilityState = if (state.shouldShowErrorDialog) {
BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.generic_error_message.asText(),
)
} else {
BasicDialogState.Hidden
},
onDismissRequest = onErrorDialogDismiss,
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
@ -210,7 +221,7 @@ private fun LoginWithDeviceScreenContent(
modifier = Modifier
.padding(horizontal = 64.dp)
.size(size = 16.dp),
)
)
} else {
BitwardenClickableText(
modifier = Modifier

View file

@ -3,12 +3,9 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -38,6 +35,7 @@ class LoginWithDeviceViewModel @Inject constructor(
override fun handleAction(action: LoginWithDeviceAction) {
when (action) {
LoginWithDeviceAction.CloseButtonClick -> handleCloseButtonClicked()
LoginWithDeviceAction.ErrorDialogDismiss -> handleErrorDialogDismissed()
LoginWithDeviceAction.ResendNotificationClick -> handleResendNotificationClicked()
LoginWithDeviceAction.ViewAllLogInOptionsClick -> handleViewAllLogInOptionsClicked()
@ -51,6 +49,19 @@ class LoginWithDeviceViewModel @Inject constructor(
sendEvent(LoginWithDeviceEvent.NavigateBack)
}
private fun handleErrorDialogDismissed() {
val viewState = mutableStateFlow.value.viewState as? LoginWithDeviceState.ViewState.Content
if (viewState != null) {
mutableStateFlow.update {
it.copy(
viewState = viewState.copy(
shouldShowErrorDialog = false,
),
)
}
}
}
private fun handleResendNotificationClicked() {
sendNewAuthRequest()
}
@ -69,17 +80,20 @@ class LoginWithDeviceViewModel @Inject constructor(
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = action.result.authRequest.fingerprint,
isResendNotificationLoading = false,
shouldShowErrorDialog = false,
),
)
}
}
is AuthRequestResult.Error -> {
// TODO BIT-1563 display error dialog
mutableStateFlow.update {
it.copy(
viewState = LoginWithDeviceState.ViewState.Error(
message = R.string.generic_error_message.asText(),
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "",
isResendNotificationLoading = false,
shouldShowErrorDialog = true,
),
)
}
@ -136,17 +150,6 @@ data class LoginWithDeviceState(
@Parcelize
data object Loading : ViewState()
/**
* Represents a state where the [LoginWithDeviceScreen] is unable to display data due to an
* error retrieving it.
*
* @property message The message to display on the error screen.
*/
@Parcelize
data class Error(
val message: Text,
) : ViewState()
/**
* Content state for the [LoginWithDeviceScreen] showing the actual content or items.
*
@ -156,6 +159,7 @@ data class LoginWithDeviceState(
data class Content(
val fingerprintPhrase: String,
val isResendNotificationLoading: Boolean,
val shouldShowErrorDialog: Boolean,
) : ViewState()
}
}
@ -186,6 +190,11 @@ sealed class LoginWithDeviceAction {
*/
data object CloseButtonClick : LoginWithDeviceAction()
/**
* Indicates that the error dialog was dismissed.
*/
data object ErrorDialogDismiss : LoginWithDeviceAction()
/**
* Indicates that the "Resend notification" text has been clicked.
*/

View file

@ -1,13 +1,15 @@
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor
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.performScrollTo
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.mockk
@ -45,6 +47,26 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
}
}
@Test
fun `dismissing error dialog should send ErrorDialogDismiss`() {
mutableStateFlow.update {
it.copy(
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "",
isResendNotificationLoading = false,
shouldShowErrorDialog = true,
),
)
}
composeTestRule
.onNodeWithText("Ok")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(LoginWithDeviceAction.ErrorDialogDismiss)
}
}
@Test
fun `resend notification click should send ResendNotificationClick action`() {
composeTestRule.onNodeWithText("Resend notification").performClick()
@ -74,36 +96,12 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
}
composeTestRule.onNode(isProgressBar).assertIsDisplayed()
mutableStateFlow.update {
it.copy(viewState = LoginWithDeviceState.ViewState.Error("Failure".asText()))
}
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
mutableStateFlow.update {
it.copy(viewState = DEFAULT_STATE.viewState)
}
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
}
@Test
fun `error should be displayed according to state`() {
val errorMessage = "error"
mutableStateFlow.update {
it.copy(viewState = LoginWithDeviceState.ViewState.Loading)
}
composeTestRule.onNodeWithText(errorMessage).assertDoesNotExist()
mutableStateFlow.update {
it.copy(viewState = LoginWithDeviceState.ViewState.Error(errorMessage.asText()))
}
composeTestRule.onNodeWithText(errorMessage).assertIsDisplayed()
mutableStateFlow.update {
it.copy(viewState = DEFAULT_STATE.viewState)
}
composeTestRule.onNodeWithText(errorMessage).assertDoesNotExist()
}
companion object {
private const val EMAIL = "test@gmail.com"
private val DEFAULT_STATE = LoginWithDeviceState(
@ -111,6 +109,7 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "alabster-drinkable-mystified-rapping-irrigate",
isResendNotificationLoading = false,
shouldShowErrorDialog = false,
),
)
}

View file

@ -2,12 +2,10 @@ package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequest
import com.x8bit.bitwarden.data.auth.repository.model.AuthRequestResult
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@ -46,6 +44,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = FINGERPRINT,
isResendNotificationLoading = false,
shouldShowErrorDialog = false,
),
)
val viewModel = createViewModel(state)
@ -82,6 +81,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = newFingerprint,
isResendNotificationLoading = false,
shouldShowErrorDialog = false,
),
),
viewModel.stateFlow.value,
@ -120,6 +120,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = newFingerprint,
isResendNotificationLoading = false,
shouldShowErrorDialog = false,
),
),
viewModel.stateFlow.value,
@ -127,7 +128,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
}
@Test
fun `on fingerprint result failure received should show error`() = runTest {
fun `on fingerprint result failure received should show error dialog`() = runTest {
val viewModel = createViewModel()
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
viewModel.actionChannel.trySend(
@ -137,8 +138,10 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
)
assertEquals(
DEFAULT_STATE.copy(
viewState = LoginWithDeviceState.ViewState.Error(
message = R.string.generic_error_message.asText(),
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = "",
isResendNotificationLoading = false,
shouldShowErrorDialog = true,
),
),
viewModel.stateFlow.value,
@ -161,6 +164,7 @@ class LoginWithDeviceViewModelTest : BaseViewModelTest() {
viewState = LoginWithDeviceState.ViewState.Content(
fingerprintPhrase = FINGERPRINT,
isResendNotificationLoading = false,
shouldShowErrorDialog = false,
),
)
private val AUTH_REQUEST = AuthRequest(