mirror of
https://github.com/bitwarden/android.git
synced 2025-02-17 04:19:54 +03:00
BIT-1563: Handle POST auth-requests error on Login with Device (#828)
This commit is contained in:
parent
ab0cfdfdc2
commit
fa551fa6ab
4 changed files with 79 additions and 56 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue