mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Add loading states and navigation events to LoginWithDeviceScreen. (#890)
This commit is contained in:
parent
a985cfaccc
commit
d9d5eaeea2
5 changed files with 123 additions and 6 deletions
|
@ -93,6 +93,12 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
|
|||
)
|
||||
loginWithDeviceDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToTwoFactorLogin = {
|
||||
navController.navigateToTwoFactorLogin(
|
||||
emailAddress = it,
|
||||
password = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
environmentDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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].
|
||||
*/
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue