diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt index 43f78f407..4ef533147 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreen.kt @@ -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.button.BitwardenFilledButton 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.text.BitwardenClickableText import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenSwitch @@ -72,6 +76,11 @@ fun TrustedDeviceScreen( } } + TrustedDeviceDialogs( + dialogState = state.dialogState, + handlers = handlers, + ) + TrustedDeviceScaffold( state = state, 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 @Composable private fun TrustedDeviceScaffold_preview() { TrustedDeviceScaffold( state = TrustedDeviceState( + dialogState = null, isRemembered = false, emailAddress = "email@bitwarden.com", environmentLabel = "vault.bitwarden.pw", @@ -207,6 +239,7 @@ private fun TrustedDeviceScaffold_preview() { ), handlers = TrustedDeviceHandlers( onBackClick = {}, + onDismissDialog = {}, onRememberToggle = {}, onContinueClick = {}, onApproveWithAdminClick = {}, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt index 46d12e993..31f3b2152 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModel.kt @@ -29,6 +29,7 @@ class TrustedDeviceViewModel @Inject constructor( val trustedDevice = account?.trustedDevice if (trustedDevice == null) authRepository.logout() TrustedDeviceState( + dialogState = null, emailAddress = account?.email.orEmpty(), environmentLabel = environmentRepository.environment.label, isRemembered = true, @@ -44,6 +45,7 @@ class TrustedDeviceViewModel @Inject constructor( override fun handleAction(action: TrustedDeviceAction) { when (action) { TrustedDeviceAction.BackClick -> handleBackClick() + TrustedDeviceAction.DismissDialog -> handleDismissDialog() is TrustedDeviceAction.RememberToggle -> handleRememberToggle(action) TrustedDeviceAction.ContinueClick -> handleContinueClick() TrustedDeviceAction.ApproveWithAdminClick -> handleApproveWithAdminClick() @@ -57,6 +59,10 @@ class TrustedDeviceViewModel @Inject constructor( authRepository.logout() } + private fun handleDismissDialog() { + mutableStateFlow.update { it.copy(dialogState = null) } + } + private fun handleRememberToggle(action: TrustedDeviceAction.RememberToggle) { mutableStateFlow.update { it.copy(isRemembered = action.isRemembered) } } @@ -89,6 +95,7 @@ class TrustedDeviceViewModel @Inject constructor( */ @Parcelize data class TrustedDeviceState( + val dialogState: DialogState?, val emailAddress: String, val environmentLabel: String, val isRemembered: Boolean, @@ -96,7 +103,29 @@ data class TrustedDeviceState( val showOtherDeviceButton: Boolean, val showRequestAdminButton: 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. @@ -131,6 +160,11 @@ sealed class TrustedDeviceAction { */ data object BackClick : TrustedDeviceAction() + /** + * User clicked to dismiss the dialog. + */ + data object DismissDialog : TrustedDeviceAction() + /** * User toggled the remember device switch. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/handlers/TrustedDeviceHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/handlers/TrustedDeviceHandlers.kt index 89c2b84de..3dde3d9d0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/handlers/TrustedDeviceHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/handlers/TrustedDeviceHandlers.kt @@ -9,6 +9,7 @@ import com.x8bit.bitwarden.ui.auth.feature.trusteddevice.TrustedDeviceViewModel */ data class TrustedDeviceHandlers( val onBackClick: () -> Unit, + val onDismissDialog: () -> Unit, val onRememberToggle: (Boolean) -> Unit, val onContinueClick: () -> Unit, val onApproveWithDeviceClick: () -> Unit, @@ -24,6 +25,7 @@ data class TrustedDeviceHandlers( fun create(viewModel: TrustedDeviceViewModel): TrustedDeviceHandlers = TrustedDeviceHandlers( onBackClick = { viewModel.trySendAction(TrustedDeviceAction.BackClick) }, + onDismissDialog = { viewModel.trySendAction(TrustedDeviceAction.DismissDialog) }, onRememberToggle = { viewModel.trySendAction(TrustedDeviceAction.RememberToggle(it)) }, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt index 27752971a..a17f5e7ed 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceScreenTest.kt @@ -1,14 +1,18 @@ 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.assertIsOff 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.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 io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -229,9 +233,47 @@ class TrustedDeviceScreenTest : BaseComposeTest() { .performScrollTo() .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( + dialogState = null, emailAddress = "email@bitwarden.com", environmentLabel = "vault.bitwarden.pw", isRemembered = false, diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt index 85cf42763..061a66b00 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/trusteddevice/TrustedDeviceViewModelTest.kt @@ -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 fun `on RememberToggle updates the isRemembered state`() = runTest { val viewModel = createViewModel() @@ -142,6 +154,7 @@ private const val USER_ID: String = "userId" private const val EMAIL: String = "email@bitwarden.com" private val DEFAULT_STATE: TrustedDeviceState = TrustedDeviceState( + dialogState = null, emailAddress = EMAIL, environmentLabel = "bitwarden.com", isRemembered = true,