diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt index 6559dc28b9..1ecf63fe6c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreen.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt index 076fe52e58..02971b4692 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModel.kt @@ -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. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt index 3680028d10..fbba1d2bcc 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceScreenTest.kt @@ -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, ), ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt index 13309bf176..5cc53ef4cd 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/loginwithdevice/LoginWithDeviceViewModelTest.kt @@ -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(