mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 19:28:44 +03:00
BIT-471 Show confirm log out dialog (#173)
This commit is contained in:
parent
5510ba64f0
commit
e2e2c60759
4 changed files with 236 additions and 6 deletions
|
@ -19,6 +19,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
|
@ -31,6 +33,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||
|
||||
/**
|
||||
* Displays the account security screen.
|
||||
|
@ -41,11 +44,30 @@ fun AccountSecurityScreen(
|
|||
onNavigateBack: () -> Unit,
|
||||
viewModel: AccountSecurityViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
AccountSecurityEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
if (state.shouldShowConfirmLogoutDialog) {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = R.string.log_out.asText(),
|
||||
message = R.string.logout_confirmation.asText(),
|
||||
confirmButtonText = R.string.yes.asText(),
|
||||
onConfirmClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.ConfirmLogoutClick) }
|
||||
},
|
||||
dismissButtonText = R.string.cancel.asText(),
|
||||
onDismissClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.DismissDialog) }
|
||||
},
|
||||
onDismissRequest = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AccountSecurityAction.DismissDialog) }
|
||||
},
|
||||
)
|
||||
}
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
|
|
|
@ -1,25 +1,70 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* View model for the account security screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class AccountSecurityViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
) : BaseViewModel<Unit, AccountSecurityEvent, AccountSecurityAction>(
|
||||
initialState = Unit,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<AccountSecurityState, AccountSecurityEvent, AccountSecurityAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: AccountSecurityState(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
),
|
||||
) {
|
||||
|
||||
init {
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: AccountSecurityAction): Unit = when (action) {
|
||||
AccountSecurityAction.LogoutClick -> authRepository.logout()
|
||||
AccountSecurityAction.BackClick -> sendEvent(AccountSecurityEvent.NavigateBack)
|
||||
AccountSecurityAction.LogoutClick -> handleLogoutClick()
|
||||
AccountSecurityAction.BackClick -> handleBackClick()
|
||||
AccountSecurityAction.ConfirmLogoutClick -> handleConfirmLogoutClick()
|
||||
AccountSecurityAction.DismissDialog -> handleDismissDialog()
|
||||
}
|
||||
|
||||
private fun handleLogoutClick() {
|
||||
mutableStateFlow.update { it.copy(shouldShowConfirmLogoutDialog = true) }
|
||||
}
|
||||
|
||||
private fun handleBackClick() = sendEvent(AccountSecurityEvent.NavigateBack)
|
||||
|
||||
private fun handleConfirmLogoutClick() {
|
||||
mutableStateFlow.update { it.copy(shouldShowConfirmLogoutDialog = false) }
|
||||
authRepository.logout()
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
mutableStateFlow.update { it.copy(shouldShowConfirmLogoutDialog = false) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models state for the Account Security screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class AccountSecurityState(
|
||||
val shouldShowConfirmLogoutDialog: Boolean,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Models events for the account security screen.
|
||||
*/
|
||||
|
@ -39,6 +84,16 @@ sealed class AccountSecurityAction {
|
|||
*/
|
||||
data object BackClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User confirmed they want to logout.
|
||||
*/
|
||||
data object ConfirmLogoutClick : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User dismissed the confirm logout dialog.
|
||||
*/
|
||||
data object DismissDialog : AccountSecurityAction()
|
||||
|
||||
/**
|
||||
* User clicked log out.
|
||||
*/
|
||||
|
|
|
@ -1,14 +1,22 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
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 com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.AccountSecurityAction.ConfirmLogoutClick
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.AccountSecurityAction.DismissDialog
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -17,6 +25,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
@Test
|
||||
fun `on Log out click should send LogoutClick`() {
|
||||
val viewModel: AccountSecurityViewModel = mockk {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AccountSecurityAction.LogoutClick) } returns Unit
|
||||
}
|
||||
|
@ -33,6 +42,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
@Test
|
||||
fun `on back click should send BackClick`() {
|
||||
val viewModel: AccountSecurityViewModel = mockk {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AccountSecurityAction.BackClick) } returns Unit
|
||||
}
|
||||
|
@ -50,6 +60,7 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
fun `on NavigateAccountSecurity should call onNavigateToAccountSecurity`() {
|
||||
var haveCalledNavigateBack = false
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns flowOf(AccountSecurityEvent.NavigateBack)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
|
@ -60,4 +71,86 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
}
|
||||
assertTrue(haveCalledNavigateBack)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `confirm dialog be shown or hidden according to the state`() {
|
||||
val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(ConfirmLogoutClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule.onNode(isDialog()).assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update { it.copy(shouldShowConfirmLogoutDialog = true) }
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Yes")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Cancel")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onNodeWithText("Are you sure you want to log out?")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on confirm logout click should send ConfirmLogoutClick`() {
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
DEFAULT_STATE.copy(shouldShowConfirmLogoutDialog = true),
|
||||
)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(ConfirmLogoutClick) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Yes")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(ConfirmLogoutClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on cancel click should send DismissDialog`() {
|
||||
val viewModel = mockk<AccountSecurityViewModel> {
|
||||
every { stateFlow } returns MutableStateFlow(
|
||||
DEFAULT_STATE.copy(shouldShowConfirmLogoutDialog = true),
|
||||
)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(DismissDialog) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
AccountSecurityScreen(
|
||||
viewModel = viewModel,
|
||||
onNavigateBack = { },
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Cancel")
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(DismissDialog) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AccountSecurityState(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
|
@ -12,9 +13,19 @@ import org.junit.jupiter.api.Test
|
|||
|
||||
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on BackClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
|
@ -24,14 +35,63 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `on LogoutClick should call logout`() = runTest {
|
||||
fun `on LogoutClick should show confirm log out dialog`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
viewModel.trySendAction(AccountSecurityAction.LogoutClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowConfirmLogoutDialog = true,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on ConfirmLogoutClick should call logout and hide confirm dialog`() = runTest {
|
||||
val authRepository: AuthRepository = mockk {
|
||||
every { logout() } returns Unit
|
||||
}
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = authRepository,
|
||||
)
|
||||
viewModel.trySendAction(AccountSecurityAction.LogoutClick)
|
||||
viewModel.trySendAction(AccountSecurityAction.ConfirmLogoutClick)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
verify { authRepository.logout() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on DismissDialog should hide dialog`() = runTest {
|
||||
val viewModel = AccountSecurityViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockk(),
|
||||
)
|
||||
viewModel.trySendAction(AccountSecurityAction.DismissDialog)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE.copy(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = AccountSecurityState(
|
||||
shouldShowConfirmLogoutDialog = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue