mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-2234: Delete Account Confirmation Screen (Navigation) (#1278)
This commit is contained in:
parent
4ac9d05036
commit
9648f720be
11 changed files with 702 additions and 18 deletions
|
@ -12,11 +12,15 @@ private const val DELETE_ACCOUNT_ROUTE = "delete_account"
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.deleteAccountDestination(
|
fun NavGraphBuilder.deleteAccountDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToDeleteAccountConfirmation: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = DELETE_ACCOUNT_ROUTE,
|
route = DELETE_ACCOUNT_ROUTE,
|
||||||
) {
|
) {
|
||||||
DeleteAccountScreen(onNavigateBack = onNavigateBack)
|
DeleteAccountScreen(
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
onNavigateToDeleteAccountConfirmation = onNavigateToDeleteAccountConfirmation,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
fun DeleteAccountScreen(
|
fun DeleteAccountScreen(
|
||||||
viewModel: DeleteAccountViewModel = hiltViewModel(),
|
viewModel: DeleteAccountViewModel = hiltViewModel(),
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToDeleteAccountConfirmation: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsState()
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -65,6 +66,10 @@ fun DeleteAccountScreen(
|
||||||
is DeleteAccountEvent.ShowToast -> {
|
is DeleteAccountEvent.ShowToast -> {
|
||||||
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeleteAccountEvent.NavigateToDeleteAccountConfirmationScreen -> {
|
||||||
|
onNavigateToDeleteAccountConfirmation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,9 +154,17 @@ fun DeleteAccountScreen(
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
DeleteAccountButton(
|
DeleteAccountButton(
|
||||||
onConfirmationClick = remember(viewModel) {
|
onDeleteAccountConfirmDialogClick = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick(it)) }
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
DeleteAccountAction.DeleteAccountConfirmDialogClick(it),
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
onDeleteAccountClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) }
|
||||||
|
},
|
||||||
|
isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.semantics { testTag = "DELETE ACCOUNT" }
|
.semantics { testTag = "DELETE ACCOUNT" }
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
@ -175,7 +188,9 @@ fun DeleteAccountScreen(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DeleteAccountButton(
|
private fun DeleteAccountButton(
|
||||||
onConfirmationClick: (masterPassword: String) -> Unit,
|
onDeleteAccountConfirmDialogClick: (masterPassword: String) -> Unit,
|
||||||
|
onDeleteAccountClick: () -> Unit,
|
||||||
|
isUnlockWithPasswordEnabled: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var showPasswordDialog by remember { mutableStateOf(false) }
|
var showPasswordDialog by remember { mutableStateOf(false) }
|
||||||
|
@ -183,7 +198,7 @@ private fun DeleteAccountButton(
|
||||||
BitwardenMasterPasswordDialog(
|
BitwardenMasterPasswordDialog(
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
showPasswordDialog = false
|
showPasswordDialog = false
|
||||||
onConfirmationClick(it)
|
onDeleteAccountConfirmDialogClick(it)
|
||||||
},
|
},
|
||||||
onDismissRequest = { showPasswordDialog = false },
|
onDismissRequest = { showPasswordDialog = false },
|
||||||
)
|
)
|
||||||
|
@ -191,7 +206,13 @@ private fun DeleteAccountButton(
|
||||||
|
|
||||||
BitwardenErrorButton(
|
BitwardenErrorButton(
|
||||||
label = stringResource(id = R.string.delete_account),
|
label = stringResource(id = R.string.delete_account),
|
||||||
onClick = { showPasswordDialog = true },
|
onClick = {
|
||||||
|
if (isUnlockWithPasswordEnabled) {
|
||||||
|
showPasswordDialog = true
|
||||||
|
} else {
|
||||||
|
onDeleteAccountClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import javax.inject.Inject
|
||||||
private const val KEY_STATE = "state"
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View model for the account security screen.
|
* View model for the [DeleteAccountScreen].
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class DeleteAccountViewModel @Inject constructor(
|
class DeleteAccountViewModel @Inject constructor(
|
||||||
|
@ -29,6 +29,10 @@ class DeleteAccountViewModel @Inject constructor(
|
||||||
) : BaseViewModel<DeleteAccountState, DeleteAccountEvent, DeleteAccountAction>(
|
) : BaseViewModel<DeleteAccountState, DeleteAccountEvent, DeleteAccountAction>(
|
||||||
initialState = savedStateHandle[KEY_STATE] ?: DeleteAccountState(
|
initialState = savedStateHandle[KEY_STATE] ?: DeleteAccountState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
isUnlockWithPasswordEnabled = requireNotNull(authRepository.userStateFlow.value)
|
||||||
|
.activeAccount
|
||||||
|
.trustedDevice
|
||||||
|
?.hasMasterPassword != false,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -42,14 +46,22 @@ class DeleteAccountViewModel @Inject constructor(
|
||||||
when (action) {
|
when (action) {
|
||||||
DeleteAccountAction.CancelClick -> handleCancelClick()
|
DeleteAccountAction.CancelClick -> handleCancelClick()
|
||||||
DeleteAccountAction.CloseClick -> handleCloseClick()
|
DeleteAccountAction.CloseClick -> handleCloseClick()
|
||||||
is DeleteAccountAction.DeleteAccountClick -> handleDeleteAccountClick(action)
|
is DeleteAccountAction.DeleteAccountClick -> handleDeleteAccountClick()
|
||||||
DeleteAccountAction.AccountDeletionConfirm -> handleAccountDeletionConfirm()
|
DeleteAccountAction.AccountDeletionConfirm -> handleAccountDeletionConfirm()
|
||||||
DeleteAccountAction.DismissDialog -> handleDismissDialog()
|
DeleteAccountAction.DismissDialog -> handleDismissDialog()
|
||||||
is DeleteAccountAction.Internal.DeleteAccountComplete -> {
|
is DeleteAccountAction.Internal.DeleteAccountComplete -> {
|
||||||
handleDeleteAccountComplete(action)
|
handleDeleteAccountComplete(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is DeleteAccountAction.DeleteAccountConfirmDialogClick -> {
|
||||||
|
handleDeleteAccountConfirmDialogClick(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeleteAccountClick() {
|
||||||
|
sendEvent(DeleteAccountEvent.NavigateToDeleteAccountConfirmationScreen)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCancelClick() {
|
private fun handleCancelClick() {
|
||||||
sendEvent(DeleteAccountEvent.NavigateBack)
|
sendEvent(DeleteAccountEvent.NavigateBack)
|
||||||
|
@ -59,7 +71,9 @@ class DeleteAccountViewModel @Inject constructor(
|
||||||
sendEvent(DeleteAccountEvent.NavigateBack)
|
sendEvent(DeleteAccountEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDeleteAccountClick(action: DeleteAccountAction.DeleteAccountClick) {
|
private fun handleDeleteAccountConfirmDialogClick(
|
||||||
|
action: DeleteAccountAction.DeleteAccountConfirmDialogClick,
|
||||||
|
) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(dialog = DeleteAccountState.DeleteAccountDialog.Loading)
|
it.copy(dialog = DeleteAccountState.DeleteAccountDialog.Loading)
|
||||||
}
|
}
|
||||||
|
@ -103,10 +117,15 @@ class DeleteAccountViewModel @Inject constructor(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models state for the Delete Account screen.
|
* Models state for the Delete Account screen.
|
||||||
|
*
|
||||||
|
* @param dialog The dialog for the [DeleteAccountScreen].
|
||||||
|
* @param isUnlockWithPasswordEnabled Whether or not the user is able to unlock the vault with
|
||||||
|
* their master password.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class DeleteAccountState(
|
data class DeleteAccountState(
|
||||||
val dialog: DeleteAccountDialog?,
|
val dialog: DeleteAccountDialog?,
|
||||||
|
val isUnlockWithPasswordEnabled: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -144,6 +163,11 @@ sealed class DeleteAccountEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : DeleteAccountEvent()
|
data object NavigateBack : DeleteAccountEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the [DeleteAccountConfirmationScreen].
|
||||||
|
*/
|
||||||
|
data object NavigateToDeleteAccountConfirmationScreen : DeleteAccountEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the [message] in a toast.
|
* Displays the [message] in a toast.
|
||||||
*/
|
*/
|
||||||
|
@ -169,7 +193,14 @@ sealed class DeleteAccountAction {
|
||||||
/**
|
/**
|
||||||
* The user has clicked the delete account button.
|
* The user has clicked the delete account button.
|
||||||
*/
|
*/
|
||||||
data class DeleteAccountClick(
|
data object DeleteAccountClick : DeleteAccountAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked the delete account confirmation dialog button.
|
||||||
|
*
|
||||||
|
* @param masterPassword The master password of the user.
|
||||||
|
*/
|
||||||
|
data class DeleteAccountConfirmDialogClick(
|
||||||
val masterPassword: String,
|
val masterPassword: String,
|
||||||
) : DeleteAccountAction()
|
) : DeleteAccountAction()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation
|
||||||
|
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
|
||||||
|
private const val DELETE_ACCOUNT_CONFIRMATION_ROUTE = "delete_account_confirmation"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add delete account confirmation destinations to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.deleteAccountConfirmationDestination(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
composableWithSlideTransitions(
|
||||||
|
route = DELETE_ACCOUNT_CONFIRMATION_ROUTE,
|
||||||
|
) {
|
||||||
|
DeleteAccountConfirmationScreen(
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the [DeleteAccountConfirmationScreen].
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToDeleteAccountConfirmation(navOptions: NavOptions? = null) {
|
||||||
|
navigate(DELETE_ACCOUNT_CONFIRMATION_ROUTE, navOptions)
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
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
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the delete account confirmation screen.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun DeleteAccountConfirmationScreen(
|
||||||
|
viewModel: DeleteAccountConfirmationViewModel = hiltViewModel(),
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val resources = context.resources
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
DeleteAccountConfirmationEvent.NavigateBack -> onNavigateBack()
|
||||||
|
|
||||||
|
is DeleteAccountConfirmationEvent.ShowToast -> {
|
||||||
|
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteAccountConfirmationDialogs(
|
||||||
|
dialogState = state.dialog,
|
||||||
|
onDeleteAccountAcknowledge = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(DeleteAccountConfirmationAction.DeleteAccountAcknowledge) }
|
||||||
|
},
|
||||||
|
onDismissDialog = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(DeleteAccountConfirmationAction.DismissDialog) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DeleteAccountConfirmationScaffold(
|
||||||
|
state = state,
|
||||||
|
onCloseClick = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(DeleteAccountConfirmationAction.CloseClick) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DeleteAccountConfirmationDialogs(
|
||||||
|
dialogState: DeleteAccountConfirmationState.DeleteAccountConfirmationDialog?,
|
||||||
|
onDismissDialog: () -> Unit,
|
||||||
|
onDeleteAccountAcknowledge: () -> Unit,
|
||||||
|
) {
|
||||||
|
when (dialogState) {
|
||||||
|
is DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = null,
|
||||||
|
message = dialogState.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = onDeleteAccountAcknowledge,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.Error -> {
|
||||||
|
BitwardenBasicDialog(
|
||||||
|
visibilityState = BasicDialogState.Shown(
|
||||||
|
title = dialogState.title,
|
||||||
|
message = dialogState.message,
|
||||||
|
),
|
||||||
|
onDismissRequest = onDismissDialog,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.Loading -> {
|
||||||
|
BitwardenLoadingDialog(
|
||||||
|
visibilityState = LoadingDialogState.Shown(dialogState.title),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun DeleteAccountConfirmationScaffold(
|
||||||
|
state: DeleteAccountConfirmationState,
|
||||||
|
onCloseClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.verification_code),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||||
|
onNavigationIconClick = onCloseClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(modifier = Modifier.padding(innerPadding)) {
|
||||||
|
// TODO finish UI in BIT-2234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
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 [DeleteAccountConfirmationScreen].
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class DeleteAccountConfirmationViewModel @Inject constructor(
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<
|
||||||
|
DeleteAccountConfirmationState,
|
||||||
|
DeleteAccountConfirmationEvent,
|
||||||
|
DeleteAccountConfirmationAction,>(
|
||||||
|
initialState = savedStateHandle[KEY_STATE] ?: DeleteAccountConfirmationState(
|
||||||
|
dialog = null,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
stateFlow
|
||||||
|
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAction(action: DeleteAccountConfirmationAction) {
|
||||||
|
when (action) {
|
||||||
|
DeleteAccountConfirmationAction.CloseClick -> handleCloseClick()
|
||||||
|
DeleteAccountConfirmationAction.DeleteAccountAcknowledge -> {
|
||||||
|
handleDeleteAccountAcknowledge()
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteAccountConfirmationAction.DismissDialog -> handleDismissDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCloseClick() {
|
||||||
|
sendEvent(DeleteAccountConfirmationEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDeleteAccountAcknowledge() {
|
||||||
|
authRepository.clearPendingAccountDeletion()
|
||||||
|
mutableStateFlow.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDismissDialog() {
|
||||||
|
mutableStateFlow.update { it.copy(dialog = null) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models state for the [DeleteAccountConfirmationScreen].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class DeleteAccountConfirmationState(
|
||||||
|
val dialog: DeleteAccountConfirmationDialog?,
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a dialog.
|
||||||
|
*/
|
||||||
|
sealed class DeleteAccountConfirmationDialog : Parcelable {
|
||||||
|
/**
|
||||||
|
* Dialog to confirm to the user that the account has been deleted.
|
||||||
|
*
|
||||||
|
* @param message The message for the dialog.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class DeleteSuccess(
|
||||||
|
val message: Text =
|
||||||
|
R.string.your_account_has_been_permanently_deleted.asText(),
|
||||||
|
) : DeleteAccountConfirmationDialog()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the error dialog when deleting an account fails.
|
||||||
|
*
|
||||||
|
* @param title The title for the dialog.
|
||||||
|
* @param message The message for the dialog.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Error(
|
||||||
|
val title: Text = R.string.an_error_has_occurred.asText(),
|
||||||
|
val message: Text,
|
||||||
|
) : DeleteAccountConfirmationDialog()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the loading dialog when deleting an account.
|
||||||
|
*
|
||||||
|
* @param title The title for the dialog.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Loading(
|
||||||
|
val title: Text = R.string.loading.asText(),
|
||||||
|
) : DeleteAccountConfirmationDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models events for the [DeleteAccountConfirmationScreen].
|
||||||
|
*/
|
||||||
|
sealed class DeleteAccountConfirmationEvent {
|
||||||
|
/**
|
||||||
|
* Navigates back.
|
||||||
|
*/
|
||||||
|
data object NavigateBack : DeleteAccountConfirmationEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the [message] in a toast.
|
||||||
|
*/
|
||||||
|
data class ShowToast(
|
||||||
|
val message: Text,
|
||||||
|
) : DeleteAccountConfirmationEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models actions for the [DeleteAccountConfirmationScreen].
|
||||||
|
*/
|
||||||
|
sealed class DeleteAccountConfirmationAction {
|
||||||
|
/**
|
||||||
|
* The user has clicked the close button.
|
||||||
|
*/
|
||||||
|
data object CloseClick : DeleteAccountConfirmationAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has dismissed the dialog.
|
||||||
|
*/
|
||||||
|
data object DismissDialog : DeleteAccountConfirmationAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has acknowledged the account deletion.
|
||||||
|
*/
|
||||||
|
data object DeleteAccountAcknowledge : DeleteAccountConfirmationAction()
|
||||||
|
}
|
|
@ -8,6 +8,8 @@ import com.x8bit.bitwarden.ui.platform.feature.search.navigateToSearch
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination
|
import com.x8bit.bitwarden.ui.platform.feature.search.searchDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.deleteAccountDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount.navigateToDeleteAccount
|
||||||
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation.deleteAccountConfirmationDestination
|
||||||
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation.navigateToDeleteAccountConfirmation
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.loginApprovalDestination
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.loginApprovalDestination
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.navigateToLoginApproval
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.loginapproval.navigateToLoginApproval
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests.navigateToPendingRequests
|
import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.pendingrequests.navigateToPendingRequests
|
||||||
|
@ -97,7 +99,15 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
deleteAccountDestination(onNavigateBack = { navController.popBackStack() })
|
deleteAccountDestination(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
onNavigateToDeleteAccountConfirmation = {
|
||||||
|
navController.navigateToDeleteAccountConfirmation()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
deleteAccountConfirmationDestination(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
)
|
||||||
loginApprovalDestination(onNavigateBack = { navController.popBackStack() })
|
loginApprovalDestination(onNavigateBack = { navController.popBackStack() })
|
||||||
pendingRequestsDestination(
|
pendingRequestsDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.junit.Test
|
||||||
class DeleteAccountScreenTest : BaseComposeTest() {
|
class DeleteAccountScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
private var onNavigateBackCalled = false
|
private var onNavigateBackCalled = false
|
||||||
|
private var onNavigateToDeleteAccountConfirmationScreenCalled = false
|
||||||
|
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<DeleteAccountEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<DeleteAccountEvent>()
|
||||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
@ -39,6 +40,9 @@ class DeleteAccountScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
DeleteAccountScreen(
|
DeleteAccountScreen(
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
onNavigateToDeleteAccountConfirmation = {
|
||||||
|
onNavigateToDeleteAccountConfirmationScreenCalled = true
|
||||||
|
},
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -50,6 +54,13 @@ class DeleteAccountScreenTest : BaseComposeTest() {
|
||||||
assertTrue(onNavigateBackCalled)
|
assertTrue(onNavigateBackCalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `NavigateToDeleteAccountConfirmationScreen should call onNavigateToDeleteAccountConfirmationScreenCalled`() {
|
||||||
|
mutableEventFlow.tryEmit(DeleteAccountEvent.NavigateToDeleteAccountConfirmationScreen)
|
||||||
|
assertTrue(onNavigateToDeleteAccountConfirmationScreenCalled)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `cancel click should emit CancelClick`() {
|
fun `cancel click should emit CancelClick`() {
|
||||||
composeTestRule.onNodeWithText("Cancel").performScrollTo().performClick()
|
composeTestRule.onNodeWithText("Cancel").performScrollTo().performClick()
|
||||||
|
@ -195,11 +206,16 @@ class DeleteAccountScreenTest : BaseComposeTest() {
|
||||||
.assertDoesNotExist()
|
.assertDoesNotExist()
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick(password))
|
viewModel.trySendAction(
|
||||||
|
DeleteAccountAction.DeleteAccountConfirmDialogClick(
|
||||||
|
password,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState(
|
private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
isUnlockWithPasswordEnabled = true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,8 @@ import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.DeleteAccountResult
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
|
@ -14,18 +16,36 @@ import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class DeleteAccountViewModelTest : BaseViewModelTest() {
|
class DeleteAccountViewModelTest : BaseViewModelTest() {
|
||||||
|
private val mutableUserStateFlow = MutableStateFlow(DEFAULT_USER_STATE)
|
||||||
private val authRepo: AuthRepository = mockk(relaxed = true)
|
private val authRepo: AuthRepository = mockk {
|
||||||
|
every { userStateFlow } returns mutableUserStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct when not set`() {
|
fun `initial state should be correct when not set`() {
|
||||||
|
mutableUserStateFlow.update { currentState ->
|
||||||
|
currentState.copy(
|
||||||
|
accounts = currentState.accounts.map { account ->
|
||||||
|
account.copy(
|
||||||
|
trustedDevice = account.trustedDevice?.copy(
|
||||||
|
hasMasterPassword = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
val viewModel = createViewModel(state = null)
|
val viewModel = createViewModel(state = null)
|
||||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
assertEquals(
|
||||||
|
DEFAULT_STATE.copy(isUnlockWithPasswordEnabled = false),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -56,13 +76,18 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on DeleteAccountClick should update dialog state when delete account succeeds`() =
|
@Suppress("MaxLineLength")
|
||||||
|
fun `on DeleteAccountConfirmDialogClick should update dialog state when delete account succeeds`() =
|
||||||
runTest {
|
runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
val masterPassword = "ckasb kcs ja"
|
val masterPassword = "ckasb kcs ja"
|
||||||
coEvery { authRepo.deleteAccount(masterPassword) } returns DeleteAccountResult.Success
|
coEvery { authRepo.deleteAccount(masterPassword) } returns DeleteAccountResult.Success
|
||||||
|
|
||||||
viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick(masterPassword))
|
viewModel.trySendAction(
|
||||||
|
DeleteAccountAction.DeleteAccountConfirmDialogClick(
|
||||||
|
masterPassword,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(dialog = DeleteAccountState.DeleteAccountDialog.DeleteSuccess),
|
DEFAULT_STATE.copy(dialog = DeleteAccountState.DeleteAccountDialog.DeleteSuccess),
|
||||||
|
@ -74,13 +99,32 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `on DeleteAccountClick should emit NavigateToDeleteAccountConfirmationScreen`() =
|
||||||
|
runTest {
|
||||||
|
val viewModel = createViewModel(
|
||||||
|
state = DEFAULT_STATE.copy(
|
||||||
|
isUnlockWithPasswordEnabled = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick)
|
||||||
|
assertEquals(
|
||||||
|
DeleteAccountEvent.NavigateToDeleteAccountConfirmationScreen,
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on DeleteAccountClick should update dialog state when deleteAccount fails`() = runTest {
|
fun `on DeleteAccountClick should update dialog state when deleteAccount fails`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
val masterPassword = "ckasb kcs ja"
|
val masterPassword = "ckasb kcs ja"
|
||||||
coEvery { authRepo.deleteAccount(masterPassword) } returns DeleteAccountResult.Error
|
coEvery { authRepo.deleteAccount(masterPassword) } returns DeleteAccountResult.Error
|
||||||
|
|
||||||
viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick(masterPassword))
|
viewModel.trySendAction(DeleteAccountAction.DeleteAccountConfirmDialogClick(masterPassword))
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(
|
DEFAULT_STATE.copy(
|
||||||
|
@ -135,6 +179,34 @@ class DeleteAccountViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_USER_STATE: UserState = UserState(
|
||||||
|
activeUserId = "activeUserId",
|
||||||
|
accounts = listOf(
|
||||||
|
UserState.Account(
|
||||||
|
userId = "activeUserId",
|
||||||
|
name = "name",
|
||||||
|
email = "email",
|
||||||
|
avatarColorHex = "avatarColorHex",
|
||||||
|
environment = Environment.Us,
|
||||||
|
isPremium = true,
|
||||||
|
isLoggedIn = true,
|
||||||
|
isVaultUnlocked = true,
|
||||||
|
needsPasswordReset = false,
|
||||||
|
isBiometricsEnabled = false,
|
||||||
|
organizations = emptyList(),
|
||||||
|
needsMasterPassword = false,
|
||||||
|
trustedDevice = UserState.TrustedDevice(
|
||||||
|
isDeviceTrusted = true,
|
||||||
|
hasMasterPassword = true,
|
||||||
|
hasAdminApproval = true,
|
||||||
|
hasLoginApprovingDevice = true,
|
||||||
|
hasResetPasswordPermission = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState(
|
private val DEFAULT_STATE: DeleteAccountState = DeleteAccountState(
|
||||||
dialog = null,
|
dialog = null,
|
||||||
|
isUnlockWithPasswordEnabled = true,
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.filterToOne
|
||||||
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
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
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DeleteAccountConfirmationScreenTest : BaseComposeTest() {
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<DeleteAccountConfirmationEvent>()
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val viewModel = mockk<DeleteAccountConfirmationViewModel>(relaxed = true) {
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
DeleteAccountConfirmationScreen(
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
viewModel = viewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateBack should call onNavigateBack`() {
|
||||||
|
mutableEventFlow.tryEmit(DeleteAccountConfirmationEvent.NavigateBack)
|
||||||
|
assertTrue(onNavigateBackCalled)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loading dialog presence should update with dialog state`() {
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Loading")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialog = DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.Loading(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Loading")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `error dialog presence should update with dialog state`() {
|
||||||
|
val message = "hello world"
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(message)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialog = DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.Error(
|
||||||
|
message = message.asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(message)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete success dialog presence should update with dialog state`() {
|
||||||
|
val message = "Your account has been permanently deleted"
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(message)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialog =
|
||||||
|
DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText(message)
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `delete success dialog dismiss should emit DeleteAccountAction`() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
dialog =
|
||||||
|
DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Ok")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(DeleteAccountConfirmationAction.DeleteAccountAcknowledge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: DeleteAccountConfirmationState =
|
||||||
|
DeleteAccountConfirmationState(dialog = null)
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccountconfirmation
|
||||||
|
|
||||||
|
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
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class DeleteAccountConfirmationViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
private val authRepo: AuthRepository = mockk(relaxed = true)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when not set`() {
|
||||||
|
val viewModel = createViewModel(state = null)
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when set`() {
|
||||||
|
val state = DEFAULT_STATE.copy(
|
||||||
|
dialog = DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.Error(
|
||||||
|
message = "Hello".asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(state = state)
|
||||||
|
assertEquals(state, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on CloseClick should emit NavigateBack`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(DeleteAccountConfirmationAction.CloseClick)
|
||||||
|
assertEquals(DeleteAccountConfirmationEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `DeleteAccountAcknowledge should clear dialog and call clearPendingAccountDeletion`() =
|
||||||
|
runTest {
|
||||||
|
every { authRepo.clearPendingAccountDeletion() } just runs
|
||||||
|
val state = DEFAULT_STATE.copy(
|
||||||
|
dialog =
|
||||||
|
DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.DeleteSuccess(),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(state = state)
|
||||||
|
|
||||||
|
viewModel.trySendAction(DeleteAccountConfirmationAction.DeleteAccountAcknowledge)
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
verify {
|
||||||
|
authRepo.clearPendingAccountDeletion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on DismissDialog should clear dialog state`() = runTest {
|
||||||
|
val state = DEFAULT_STATE.copy(
|
||||||
|
dialog = DeleteAccountConfirmationState.DeleteAccountConfirmationDialog.Error(
|
||||||
|
message = "Hello".asText(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel(state = state)
|
||||||
|
|
||||||
|
viewModel.trySendAction(DeleteAccountConfirmationAction.DismissDialog)
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(
|
||||||
|
authenticationRepository: AuthRepository = authRepo,
|
||||||
|
state: DeleteAccountConfirmationState? = null,
|
||||||
|
): DeleteAccountConfirmationViewModel = DeleteAccountConfirmationViewModel(
|
||||||
|
authRepository = authenticationRepository,
|
||||||
|
savedStateHandle = SavedStateHandle().apply { set("state", state) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: DeleteAccountConfirmationState =
|
||||||
|
DeleteAccountConfirmationState(dialog = null)
|
Loading…
Reference in a new issue