Add loading states and navigation events to LoginWithDeviceScreen. (#890)

This commit is contained in:
David Perez 2024-01-31 01:41:53 -06:00 committed by Álison Fernandes
parent a985cfaccc
commit d9d5eaeea2
5 changed files with 123 additions and 6 deletions

View file

@ -93,6 +93,12 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
)
loginWithDeviceDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToTwoFactorLogin = {
navController.navigateToTwoFactorLogin(
emailAddress = it,
password = null,
)
},
)
environmentDestination(
onNavigateBack = { navController.popBackStack() },

View file

@ -36,12 +36,14 @@ fun NavController.navigateToLoginWithDevice(
*/
fun NavGraphBuilder.loginWithDeviceDestination(
onNavigateBack: () -> Unit,
onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit,
) {
composableWithSlideTransitions(
route = LOGIN_WITH_DEVICE_ROUTE,
) {
LoginWithDeviceScreen(
onNavigateBack = onNavigateBack,
onNavigateToTwoFactorLogin = onNavigateToTwoFactorLogin,
)
}
}

View file

@ -42,8 +42,12 @@ 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.BitwardenLoadingContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors
import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
@ -55,13 +59,23 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography
@Composable
fun LoginWithDeviceScreen(
onNavigateBack: () -> Unit,
onNavigateToTwoFactorLogin: (emailAddress: String) -> Unit,
viewModel: LoginWithDeviceViewModel = hiltViewModel(),
intentManager: IntentManager = LocalIntentManager.current,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
EventsEffect(viewModel = viewModel) { event ->
when (event) {
LoginWithDeviceEvent.NavigateBack -> onNavigateBack()
is LoginWithDeviceEvent.NavigateToCaptcha -> {
intentManager.startCustomTabsActivity(uri = event.uri)
}
is LoginWithDeviceEvent.NavigateToTwoFactorLogin -> {
onNavigateToTwoFactorLogin(event.emailAddress)
}
is LoginWithDeviceEvent.ShowToast -> {
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
}
@ -250,6 +264,10 @@ private fun LoginWithDeviceDialogs(
onDismissDialog: () -> Unit,
) {
when (state) {
is LoginWithDeviceState.DialogState.Loading -> BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(text = state.message),
)
is LoginWithDeviceState.DialogState.Error -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = state.title,

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import android.net.Uri
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
@ -47,10 +48,7 @@ class LoginWithDeviceViewModel @Inject constructor(
LoginWithDeviceAction.DismissDialog -> handleErrorDialogDismissed()
LoginWithDeviceAction.ResendNotificationClick -> handleResendNotificationClicked()
LoginWithDeviceAction.ViewAllLogInOptionsClick -> handleViewAllLogInOptionsClicked()
is LoginWithDeviceAction.Internal.NewAuthRequestResultReceive -> {
handleNewAuthRequestResultReceived(action)
}
is LoginWithDeviceAction.Internal -> handleInternalActions(action)
}
}
@ -70,6 +68,14 @@ class LoginWithDeviceViewModel @Inject constructor(
sendEvent(LoginWithDeviceEvent.NavigateBack)
}
private fun handleInternalActions(action: LoginWithDeviceAction.Internal) {
when (action) {
is LoginWithDeviceAction.Internal.NewAuthRequestResultReceive -> {
handleNewAuthRequestResultReceived(action)
}
}
}
@Suppress("LongMethod")
private fun handleNewAuthRequestResultReceived(
action: LoginWithDeviceAction.Internal.NewAuthRequestResultReceive,
@ -211,6 +217,14 @@ data class LoginWithDeviceState(
* Represents the current state of any dialogs on the screen.
*/
sealed class DialogState : Parcelable {
/**
* Displays an loading dialog to the user.
*/
@Parcelize
data class Loading(
val message: Text,
) : DialogState()
/**
* Displays an error dialog to the user.
*/
@ -231,6 +245,18 @@ sealed class LoginWithDeviceEvent {
*/
data object NavigateBack : LoginWithDeviceEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToCaptcha(val uri: Uri) : LoginWithDeviceEvent()
/**
* Navigates to the two-factor login screen.
*/
data class NavigateToTwoFactorLogin(
val emailAddress: String,
) : LoginWithDeviceEvent()
/**
* Shows a toast with the given [message].
*/

View file

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.auth.feature.loginwithdevice
import android.net.Uri
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.hasAnyAncestor
@ -12,18 +13,27 @@ import com.x8bit.bitwarden.R
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.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.util.isProgressBar
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import junit.framework.TestCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class LoginWithDeviceScreenTest : BaseComposeTest() {
private var onNavigateBackCalled = false
private var onNavigateToTwoFactorLoginEmail: String? = null
private val intentManager: IntentManager = mockk {
every { startCustomTabsActivity(any()) } just runs
}
private val mutableEventFlow = bufferedMutableSharedFlow<LoginWithDeviceEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
private val viewModel = mockk<LoginWithDeviceViewModel>(relaxed = true) {
@ -36,7 +46,9 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
composeTestRule.setContent {
LoginWithDeviceScreen(
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToTwoFactorLogin = { onNavigateToTwoFactorLoginEmail = it },
viewModel = viewModel,
intentManager = intentManager,
)
}
}
@ -87,7 +99,23 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
@Test
fun `NavigateBack should call onNavigateBack`() {
mutableEventFlow.tryEmit(LoginWithDeviceEvent.NavigateBack)
TestCase.assertTrue(onNavigateBackCalled)
assertTrue(onNavigateBackCalled)
}
@Test
fun `NavigateBack should call onNavigateToTwoFactorLoginEmail`() {
val email = "test@email.com"
mutableEventFlow.tryEmit(LoginWithDeviceEvent.NavigateToTwoFactorLogin(email))
assertEquals(email, onNavigateToTwoFactorLoginEmail)
}
@Test
fun `NavigateToCaptcha should call launchUri on intentManager`() {
val uri = mockk<Uri>()
mutableEventFlow.tryEmit(LoginWithDeviceEvent.NavigateToCaptcha(uri))
verify(exactly = 1) {
intentManager.startCustomTabsActivity(uri)
}
}
@Test
@ -102,6 +130,43 @@ class LoginWithDeviceScreenTest : BaseComposeTest() {
}
composeTestRule.onNode(isProgressBar).assertDoesNotExist()
}
@Test
fun `progress dialog should be displayed according to state`() {
val loadingMessage = "loading..."
mutableStateFlow.update {
it.copy(
dialogState = LoginWithDeviceState.DialogState.Loading(loadingMessage.asText()),
)
}
composeTestRule
.onNodeWithText(loadingMessage)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
mutableStateFlow.update { it.copy(dialogState = null) }
composeTestRule.onNode(isDialog()).assertDoesNotExist()
}
@Test
fun `error dialog should be displayed according to state`() {
val errorMessage = "Error"
mutableStateFlow.update {
it.copy(
dialogState = LoginWithDeviceState.DialogState.Error(
title = null,
message = errorMessage.asText(),
),
)
}
composeTestRule
.onNodeWithText(errorMessage)
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
mutableStateFlow.update { it.copy(dialogState = null) }
composeTestRule.onNode(isDialog()).assertDoesNotExist()
}
}
private const val EMAIL = "test@gmail.com"