mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Add dialog support for TrustedDeviceScreen (#1239)
This commit is contained in:
parent
a6a4c40693
commit
79f8703d9b
5 changed files with 125 additions and 1 deletions
|
@ -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 = {},
|
||||||
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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))
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue