Add dialog support for TrustedDeviceScreen (#1239)

This commit is contained in:
David Perez 2024-04-08 13:03:58 -05:00 committed by Álison Fernandes
parent a6a4c40693
commit 79f8703d9b
5 changed files with 125 additions and 1 deletions

View file

@ -37,6 +37,10 @@ import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch
@ -72,6 +76,11 @@ fun TrustedDeviceScreen(
} }
} }
TrustedDeviceDialogs(
dialogState = state.dialogState,
handlers = handlers,
)
TrustedDeviceScaffold( TrustedDeviceScaffold(
state = state, state = state,
handlers = handlers, handlers = handlers,
@ -192,11 +201,34 @@ private fun TrustedDeviceScaffold(
} }
} }
@Composable
private fun TrustedDeviceDialogs(
dialogState: TrustedDeviceState.DialogState?,
handlers: TrustedDeviceHandlers,
) {
when (dialogState) {
is TrustedDeviceState.DialogState.Error -> BitwardenBasicDialog(
visibilityState = BasicDialogState.Shown(
title = dialogState.title,
message = dialogState.message,
),
onDismissRequest = handlers.onDismissDialog,
)
is TrustedDeviceState.DialogState.Loading -> BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(dialogState.message),
)
null -> Unit
}
}
@Preview @Preview
@Composable @Composable
private fun TrustedDeviceScaffold_preview() { private fun TrustedDeviceScaffold_preview() {
TrustedDeviceScaffold( TrustedDeviceScaffold(
state = TrustedDeviceState( state = TrustedDeviceState(
dialogState = null,
isRemembered = false, isRemembered = false,
emailAddress = "email@bitwarden.com", emailAddress = "email@bitwarden.com",
environmentLabel = "vault.bitwarden.pw", environmentLabel = "vault.bitwarden.pw",
@ -207,6 +239,7 @@ private fun TrustedDeviceScaffold_preview() {
), ),
handlers = TrustedDeviceHandlers( handlers = TrustedDeviceHandlers(
onBackClick = {}, onBackClick = {},
onDismissDialog = {},
onRememberToggle = {}, onRememberToggle = {},
onContinueClick = {}, onContinueClick = {},
onApproveWithAdminClick = {}, onApproveWithAdminClick = {},

View file

@ -29,6 +29,7 @@ class TrustedDeviceViewModel @Inject constructor(
val trustedDevice = account?.trustedDevice val trustedDevice = account?.trustedDevice
if (trustedDevice == null) authRepository.logout() if (trustedDevice == null) authRepository.logout()
TrustedDeviceState( TrustedDeviceState(
dialogState = null,
emailAddress = account?.email.orEmpty(), emailAddress = account?.email.orEmpty(),
environmentLabel = environmentRepository.environment.label, environmentLabel = environmentRepository.environment.label,
isRemembered = true, isRemembered = true,
@ -44,6 +45,7 @@ class TrustedDeviceViewModel @Inject constructor(
override fun handleAction(action: TrustedDeviceAction) { override fun handleAction(action: TrustedDeviceAction) {
when (action) { when (action) {
TrustedDeviceAction.BackClick -> handleBackClick() TrustedDeviceAction.BackClick -> handleBackClick()
TrustedDeviceAction.DismissDialog -> handleDismissDialog()
is TrustedDeviceAction.RememberToggle -> handleRememberToggle(action) is TrustedDeviceAction.RememberToggle -> handleRememberToggle(action)
TrustedDeviceAction.ContinueClick -> handleContinueClick() TrustedDeviceAction.ContinueClick -> handleContinueClick()
TrustedDeviceAction.ApproveWithAdminClick -> handleApproveWithAdminClick() TrustedDeviceAction.ApproveWithAdminClick -> handleApproveWithAdminClick()
@ -57,6 +59,10 @@ class TrustedDeviceViewModel @Inject constructor(
authRepository.logout() authRepository.logout()
} }
private fun handleDismissDialog() {
mutableStateFlow.update { it.copy(dialogState = null) }
}
private fun handleRememberToggle(action: TrustedDeviceAction.RememberToggle) { private fun handleRememberToggle(action: TrustedDeviceAction.RememberToggle) {
mutableStateFlow.update { it.copy(isRemembered = action.isRemembered) } mutableStateFlow.update { it.copy(isRemembered = action.isRemembered) }
} }
@ -89,6 +95,7 @@ class TrustedDeviceViewModel @Inject constructor(
*/ */
@Parcelize @Parcelize
data class TrustedDeviceState( data class TrustedDeviceState(
val dialogState: DialogState?,
val emailAddress: String, val emailAddress: String,
val environmentLabel: String, val environmentLabel: String,
val isRemembered: Boolean, val isRemembered: Boolean,
@ -96,7 +103,29 @@ data class TrustedDeviceState(
val showOtherDeviceButton: Boolean, val showOtherDeviceButton: Boolean,
val showRequestAdminButton: Boolean, val showRequestAdminButton: Boolean,
val showMasterPasswordButton: Boolean, val showMasterPasswordButton: Boolean,
) : Parcelable ) : Parcelable {
/**
* Represents the current state of any dialogs on the screen.
*/
sealed class DialogState : Parcelable {
/**
* Represents a dismissible dialog with the given error [message].
*/
@Parcelize
data class Error(
val title: Text?,
val message: Text,
) : DialogState()
/**
* Represents a loading dialog with the given [message].
*/
@Parcelize
data class Loading(
val message: Text,
) : DialogState()
}
}
/** /**
* Models events for the Trusted Device screen. * Models events for the Trusted Device screen.
@ -131,6 +160,11 @@ sealed class TrustedDeviceAction {
*/ */
data object BackClick : TrustedDeviceAction() data object BackClick : TrustedDeviceAction()
/**
* User clicked to dismiss the dialog.
*/
data object DismissDialog : TrustedDeviceAction()
/** /**
* User toggled the remember device switch. * User toggled the remember device switch.
*/ */

View file

@ -9,6 +9,7 @@ import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TrustedDeviceViewModel
*/ */
data class TrustedDeviceHandlers( data class TrustedDeviceHandlers(
val onBackClick: () -> Unit, val onBackClick: () -> Unit,
val onDismissDialog: () -> Unit,
val onRememberToggle: (Boolean) -> Unit, val onRememberToggle: (Boolean) -> Unit,
val onContinueClick: () -> Unit, val onContinueClick: () -> Unit,
val onApproveWithDeviceClick: () -> Unit, val onApproveWithDeviceClick: () -> Unit,
@ -24,6 +25,7 @@ data class TrustedDeviceHandlers(
fun create(viewModel: TrustedDeviceViewModel): TrustedDeviceHandlers = fun create(viewModel: TrustedDeviceViewModel): TrustedDeviceHandlers =
TrustedDeviceHandlers( TrustedDeviceHandlers(
onBackClick = { viewModel.trySendAction(TrustedDeviceAction.BackClick) }, onBackClick = { viewModel.trySendAction(TrustedDeviceAction.BackClick) },
onDismissDialog = { viewModel.trySendAction(TrustedDeviceAction.DismissDialog) },
onRememberToggle = { onRememberToggle = {
viewModel.trySendAction(TrustedDeviceAction.RememberToggle(it)) viewModel.trySendAction(TrustedDeviceAction.RememberToggle(it))
}, },

View file

@ -1,14 +1,18 @@
package com.x8bit.bitwarden.ui.auth.feature.trusteddevice package com.x8bit.bitwarden.ui.auth.feature.trusteddevice
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff import androidx.compose.ui.test.assertIsOff
import androidx.compose.ui.test.assertIsOn import androidx.compose.ui.test.assertIsOn
import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performScrollTo
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify import io.mockk.verify
@ -229,9 +233,47 @@ class TrustedDeviceScreenTest : BaseComposeTest() {
.performScrollTo() .performScrollTo()
.assertIsDisplayed() .assertIsDisplayed()
} }
@Test
fun `dialog should update according to state`() {
composeTestRule.onNode(isDialog()).assertDoesNotExist()
mutableStateFlow.update {
it.copy(
dialogState = TrustedDeviceState.DialogState.Loading(message = "Loading".asText()),
)
}
composeTestRule.onNode(isDialog()).assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "Loading")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
mutableStateFlow.update {
it.copy(
dialogState = TrustedDeviceState.DialogState.Error(
title = "Hello".asText(),
message = "World".asText(),
),
)
}
composeTestRule.onNode(isDialog()).assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "Hello")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText(text = "World")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
mutableStateFlow.update { it.copy(dialogState = null) }
composeTestRule.onNode(isDialog()).assertDoesNotExist()
}
} }
private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState( private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState(
dialogState = null,
emailAddress = "email@bitwarden.com", emailAddress = "email@bitwarden.com",
environmentLabel = "vault.bitwarden.pw", environmentLabel = "vault.bitwarden.pw",
isRemembered = false, isRemembered = false,

View file

@ -51,6 +51,18 @@ class TrustedDeviceViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `on DismissDialog should clear dialogState`() {
val initialState = DEFAULT_STATE.copy(
dialogState = TrustedDeviceState.DialogState.Loading("Loading".asText()),
)
val viewModel = createViewModel(initialState)
viewModel.trySendAction(TrustedDeviceAction.DismissDialog)
assertEquals(initialState.copy(dialogState = null), viewModel.stateFlow.value)
}
@Test @Test
fun `on RememberToggle updates the isRemembered state`() = runTest { fun `on RememberToggle updates the isRemembered state`() = runTest {
val viewModel = createViewModel() val viewModel = createViewModel()
@ -142,6 +154,7 @@ private const val USER_ID: String = "userId"
private const val EMAIL: String = "email@bitwarden.com" private const val EMAIL: String = "email@bitwarden.com"
private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState( private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState(
dialogState = null,
emailAddress = EMAIL, emailAddress = EMAIL,
environmentLabel = "bitwarden.com", environmentLabel = "bitwarden.com",
isRemembered = true, isRemembered = true,