From 715ff06dfc52055d57ec458c9659f6d5ab04fc3e Mon Sep 17 00:00:00 2001 From: David Perez Date: Mon, 13 Nov 2023 13:17:16 -0600 Subject: [PATCH] BIT-1031: Add shell for the delete account UI (#241) --- .../components/BitwardenErrorButton.kt | 72 ++++++++++ .../components/BitwardenOutlinedButton.kt | 65 +++++++++ .../feature/settings/SettingsNavigation.kt | 6 +- .../AccountSecurityNavigation.kt | 6 +- .../accountsecurity/AccountSecurityScreen.kt | 3 + .../AccountSecurityViewModel.kt | 8 +- .../deleteaccount/DeleteAccountNavigation.kt | 33 +++++ .../deleteaccount/DeleteAccountScreen.kt | 130 ++++++++++++++++++ .../deleteaccount/DeleteAccountViewModel.kt | 75 ++++++++++ .../vaultunlocked/VaultUnlockedNavigation.kt | 4 + .../VaultUnlockedNavBarNavigation.kt | 2 + .../VaultUnlockedNavBarScreen.kt | 8 +- app/src/main/res/drawable/ic_warning.xml | 20 +++ .../AccountSecurityScreenTest.kt | 8 ++ .../AccountSecurityViewModelTest.kt | 7 +- .../deleteaccount/DeleteAccountScreenTest.kt | 40 ++++++ .../DeleteAccountViewModelTest.kt | 43 ++++++ .../VaultUnlockedNavBarScreenTest.kt | 8 ++ 18 files changed, 528 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenErrorButton.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenOutlinedButton.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt create mode 100644 app/src/main/res/drawable/ic_warning.xml create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt create mode 100644 app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenErrorButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenErrorButton.kt new file mode 100644 index 000000000..fe0ccd886 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenErrorButton.kt @@ -0,0 +1,72 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme + +/** + * Represents a Bitwarden-styled filled [Button] for error scenarios. + * + * @param label The label for the button. + * @param onClick The callback when the button is clicked. + * @param modifier The [Modifier] to be applied to the button. + * @param isEnabled Whether or not the button is enabled. + */ +@Composable +fun BitwardenErrorButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + Button( + onClick = onClick, + modifier = modifier.semantics(mergeDescendants = true) {}, + enabled = isEnabled, + contentPadding = PaddingValues( + vertical = 10.dp, + horizontal = 24.dp, + ), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + ) + } +} + +@Preview +@Composable +private fun BitwardenErrorButton_preview_isEnabled() { + BitwardenTheme { + BitwardenErrorButton( + label = "Label", + onClick = {}, + isEnabled = true, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BitwardenErrorButton_preview_isNotEnabled() { + BitwardenTheme { + BitwardenErrorButton( + label = "Label", + onClick = {}, + isEnabled = false, + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenOutlinedButton.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenOutlinedButton.kt new file mode 100644 index 000000000..4a0d539a9 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenOutlinedButton.kt @@ -0,0 +1,65 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +/** + * Represents a Bitwarden-styled filled [OutlinedButton]. + * + * @param label The label for the button. + * @param onClick The callback when the button is clicked. + * @param modifier The [Modifier] to be applied to the button. + * @param isEnabled Whether or not the button is enabled. + */ +@Composable +fun BitwardenOutlinedButton( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + OutlinedButton( + onClick = onClick, + modifier = modifier + .semantics(mergeDescendants = true) { }, + enabled = isEnabled, + contentPadding = PaddingValues( + vertical = 10.dp, + horizontal = 24.dp, + ), + colors = ButtonDefaults.outlinedButtonColors(), + ) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + ) + } +} + +@Preview +@Composable +private fun BitwardenOutlinedButton_preview_isEnabled() { + BitwardenOutlinedButton( + label = "Label", + onClick = {}, + isEnabled = true, + ) +} + +@Preview +@Composable +private fun BitwardenOutlinedButton_preview_isNotEnabled() { + BitwardenOutlinedButton( + label = "Label", + onClick = {}, + isEnabled = false, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt index add6f90a0..0b31bc1f8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/SettingsNavigation.kt @@ -27,6 +27,7 @@ private const val SETTINGS_ROUTE: String = "settings" */ fun NavGraphBuilder.settingsGraph( navController: NavController, + onNavigateToDeleteAccount: () -> Unit, ) { navigation( startDestination = SETTINGS_ROUTE, @@ -49,7 +50,10 @@ fun NavGraphBuilder.settingsGraph( ) } aboutDestination(onNavigateBack = { navController.popBackStack() }) - accountSecurityDestination(onNavigateBack = { navController.popBackStack() }) + accountSecurityDestination( + onNavigateBack = { navController.popBackStack() }, + onNavigateToDeleteAccount = onNavigateToDeleteAccount, + ) appearanceDestination(onNavigateBack = { navController.popBackStack() }) autoFillDestination(onNavigateBack = { navController.popBackStack() }) otherDestination(onNavigateBack = { navController.popBackStack() }) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt index 97aea094b..31533bdac 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityNavigation.kt @@ -13,6 +13,7 @@ private const val ACCOUNT_SECURITY_ROUTE = "settings_account_security" */ fun NavGraphBuilder.accountSecurityDestination( onNavigateBack: () -> Unit, + onNavigateToDeleteAccount: () -> Unit, ) { composable( route = ACCOUNT_SECURITY_ROUTE, @@ -21,7 +22,10 @@ fun NavGraphBuilder.accountSecurityDestination( popEnterTransition = TransitionProviders.Enter.pushLeft, popExitTransition = TransitionProviders.Exit.pushRight, ) { - AccountSecurityScreen(onNavigateBack = onNavigateBack) + AccountSecurityScreen( + onNavigateBack = onNavigateBack, + onNavigateToDeleteAccount = onNavigateToDeleteAccount, + ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt index 48902c2bb..d46e2f071 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreen.kt @@ -52,6 +52,7 @@ import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialTypography @Composable fun AccountSecurityScreen( onNavigateBack: () -> Unit, + onNavigateToDeleteAccount: () -> Unit, viewModel: AccountSecurityViewModel = hiltViewModel(), intentHandler: IntentHandler = IntentHandler(context = LocalContext.current), ) { @@ -62,6 +63,8 @@ fun AccountSecurityScreen( when (event) { AccountSecurityEvent.NavigateBack -> onNavigateBack() + AccountSecurityEvent.NavigateToDeleteAccount -> onNavigateToDeleteAccount() + AccountSecurityEvent.NavigateToFingerprintPhrase -> { intentHandler.launchUri("http://bitwarden.com/help/fingerprint-phrase".toUri()) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt index 442506987..aed448a1f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModel.kt @@ -87,8 +87,7 @@ class AccountSecurityViewModel @Inject constructor( } private fun handleDeleteAccountClick() { - // TODO BIT-1031: Navigate to delete account - sendEvent(AccountSecurityEvent.ShowToast("Not yet implemented.".asText())) + sendEvent(AccountSecurityEvent.NavigateToDeleteAccount) } private fun handleDismissDialog() { @@ -215,6 +214,11 @@ sealed class AccountSecurityEvent { */ data object NavigateBack : AccountSecurityEvent() + /** + * Navigate to the delete account screen. + */ + data object NavigateToDeleteAccount : AccountSecurityEvent() + /** * Navigate to fingerprint phrase information. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt new file mode 100644 index 000000000..0328d151a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountNavigation.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount + +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders + +private const val DELETE_ACCOUNT_ROUTE = "delete_account" + +/** + * Add delete account destinations to the nav graph. + */ +fun NavGraphBuilder.deleteAccountDestination( + onNavigateBack: () -> Unit, +) { + composable( + route = DELETE_ACCOUNT_ROUTE, + enterTransition = TransitionProviders.Enter.slideUp, + exitTransition = TransitionProviders.Exit.slideDown, + popEnterTransition = TransitionProviders.Enter.slideUp, + popExitTransition = TransitionProviders.Exit.slideDown, + ) { + DeleteAccountScreen(onNavigateBack = onNavigateBack) + } +} + +/** + * Navigate to the delete account screen. + */ +fun NavController.navigateToDeleteAccount(navOptions: NavOptions? = null) { + navigate(DELETE_ACCOUNT_ROUTE, navOptions) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt new file mode 100644 index 000000000..048eeec14 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreen.kt @@ -0,0 +1,130 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount + +import android.widget.Toast +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +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.compose.ui.unit.dp +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.BitwardenErrorButton +import com.x8bit.bitwarden.ui.platform.components.BitwardenOutlinedButton +import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold +import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar + +/** + * Displays the delete account screen. + */ +@Suppress("LongMethod") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeleteAccountScreen( + viewModel: DeleteAccountViewModel = hiltViewModel(), + onNavigateBack: () -> Unit, +) { + val context = LocalContext.current + val resources = context.resources + EventsEffect(viewModel = viewModel) { event -> + when (event) { + DeleteAccountEvent.NavigateBack -> onNavigateBack() + + is DeleteAccountEvent.ShowToast -> { + Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show() + } + } + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + BitwardenScaffold( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + BitwardenTopAppBar( + title = stringResource(id = R.string.delete_account), + scrollBehavior = scrollBehavior, + navigationIcon = painterResource(id = R.drawable.ic_close), + navigationIconContentDescription = stringResource(id = R.string.close), + onNavigationIconClick = remember(viewModel) { + { viewModel.trySendAction(DeleteAccountAction.CloseClick) } + }, + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier + .imePadding() + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()), + ) { + Spacer(modifier = Modifier.height(8.dp)) + Icon( + painter = painterResource(id = R.drawable.ic_warning), + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(id = R.string.deleting_your_account_is_permanent), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(id = R.string.delete_account_explanation), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(24.dp)) + BitwardenErrorButton( + label = stringResource(id = R.string.delete_account), + onClick = remember(viewModel) { + { viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.height(12.dp)) + BitwardenOutlinedButton( + label = stringResource(id = R.string.cancel), + onClick = remember(viewModel) { + { viewModel.trySendAction(DeleteAccountAction.CancelClick) } + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) + Spacer(modifier = Modifier.navigationBarsPadding()) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt new file mode 100644 index 000000000..cd85f8576 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModel.kt @@ -0,0 +1,75 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount + +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 javax.inject.Inject + +/** + * View model for the account security screen. + */ +@HiltViewModel +class DeleteAccountViewModel @Inject constructor() : + BaseViewModel( + initialState = Unit, + ) { + + override fun handleAction(action: DeleteAccountAction) { + when (action) { + DeleteAccountAction.CancelClick -> handleCancelClick() + DeleteAccountAction.CloseClick -> handleCloseClick() + DeleteAccountAction.DeleteAccountClick -> handleDeleteAccountClick() + } + } + + private fun handleCancelClick() { + sendEvent(DeleteAccountEvent.NavigateBack) + } + + private fun handleCloseClick() { + sendEvent(DeleteAccountEvent.NavigateBack) + } + + private fun handleDeleteAccountClick() { + // TODO: Delete the users account (BIT-1111) + sendEvent(DeleteAccountEvent.ShowToast("Not yet implemented.".asText())) + } +} + +/** + * Models events for the delete account screen. + */ +sealed class DeleteAccountEvent { + /** + * Navigates back. + */ + data object NavigateBack : DeleteAccountEvent() + + /** + * Displays the [message] in a toast. + */ + data class ShowToast( + val message: Text, + ) : DeleteAccountEvent() +} + +/** + * Models actions for the delete account screen. + */ +sealed class DeleteAccountAction { + /** + * The user has clicked the cancel button. + */ + data object CancelClick : DeleteAccountAction() + + /** + * The user has clicked the close button. + */ + data object CloseClick : DeleteAccountAction() + + /** + * The user has clicked the delete account button. + */ + data object DeleteAccountClick : DeleteAccountAction() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index 3a86a19f8..d768145b7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -4,6 +4,8 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.navigation +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.vaultunlockednavbar.VAULT_UNLOCKED_NAV_BAR_ROUTE import com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar.vaultUnlockedNavBarDestination import com.x8bit.bitwarden.ui.tools.feature.send.navigateToNewSend @@ -33,7 +35,9 @@ fun NavGraphBuilder.vaultUnlockedGraph( vaultUnlockedNavBarDestination( onNavigateToVaultAddItem = { navController.navigateToVaultAddItem() }, onNavigateToNewSend = { navController.navigateToNewSend() }, + onNavigateToDeleteAccount = { navController.navigateToDeleteAccount() }, ) + deleteAccountDestination(onNavigateBack = { navController.popBackStack() }) vaultAddItemDestination(onNavigateBack = { navController.popBackStack() }) newSendDestination(onNavigateBack = { navController.popBackStack() }) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index 05f7f8cb5..7a40e1ef7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -24,6 +24,7 @@ fun NavController.navigateToVaultUnlockedNavBar(navOptions: NavOptions? = null) fun NavGraphBuilder.vaultUnlockedNavBarDestination( onNavigateToVaultAddItem: () -> Unit, onNavigateToNewSend: () -> Unit, + onNavigateToDeleteAccount: () -> Unit, ) { composable( route = VAULT_UNLOCKED_NAV_BAR_ROUTE, @@ -35,6 +36,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination( VaultUnlockedNavBarScreen( onNavigateToVaultAddItem = onNavigateToVaultAddItem, onNavigateToNewSend = onNavigateToNewSend, + onNavigateToDeleteAccount = onNavigateToDeleteAccount, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 9850733a7..2cf11d612 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -67,6 +67,7 @@ fun VaultUnlockedNavBarScreen( navController: NavHostController = rememberNavController(), onNavigateToVaultAddItem: () -> Unit, onNavigateToNewSend: () -> Unit, + onNavigateToDeleteAccount: () -> Unit, ) { EventsEffect(viewModel = viewModel) { event -> navController.apply { @@ -94,6 +95,7 @@ fun VaultUnlockedNavBarScreen( navController = navController, navigateToVaultAddItem = onNavigateToVaultAddItem, navigateToNewSend = onNavigateToNewSend, + navigateToDeleteAccount = onNavigateToDeleteAccount, generatorTabClickedAction = { viewModel.trySendAction(VaultUnlockedNavBarAction.GeneratorTabClick) }, @@ -122,6 +124,7 @@ private fun VaultUnlockedNavBarScaffold( settingsTabClickedAction: () -> Unit, navigateToVaultAddItem: () -> Unit, navigateToNewSend: () -> Unit, + navigateToDeleteAccount: () -> Unit, ) { var shouldDimNavBar by remember { mutableStateOf(false) } @@ -181,7 +184,10 @@ private fun VaultUnlockedNavBarScaffold( ) sendGraph(onNavigateToNewSend = navigateToNewSend) generatorDestination() - settingsGraph(navController) + settingsGraph( + navController = navController, + onNavigateToDeleteAccount = navigateToDeleteAccount, + ) } } } diff --git a/app/src/main/res/drawable/ic_warning.xml b/app/src/main/res/drawable/ic_warning.xml new file mode 100644 index 000000000..bb708159b --- /dev/null +++ b/app/src/main/res/drawable/ic_warning.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt index 79f993d43..994b5ea8e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityScreenTest.kt @@ -33,6 +33,7 @@ import org.junit.Test class AccountSecurityScreenTest : BaseComposeTest() { private var onNavigateBackCalled = false + private var onNavigateToDeleteAccountCalled = false private val intentHandler = mockk { every { launchUri(any()) } just runs @@ -51,6 +52,7 @@ class AccountSecurityScreenTest : BaseComposeTest() { composeTestRule.setContent { AccountSecurityScreen( onNavigateBack = { onNavigateBackCalled = true }, + onNavigateToDeleteAccount = { onNavigateToDeleteAccountCalled = true }, viewModel = viewModel, intentHandler = intentHandler, ) @@ -236,6 +238,12 @@ class AccountSecurityScreenTest : BaseComposeTest() { assertTrue(onNavigateBackCalled) } + @Test + fun `on NavigateToDeleteAccount should call onNavigateToDeleteAccountCalled`() { + mutableEventFlow.tryEmit(AccountSecurityEvent.NavigateToDeleteAccount) + assertTrue(onNavigateToDeleteAccountCalled) + } + @Test fun `confirm dialog be shown or hidden according to the state`() { composeTestRule.onNode(isDialog()).assertDoesNotExist() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt index 3277e37fc..48fd4f2e6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/AccountSecurityViewModelTest.kt @@ -62,14 +62,11 @@ class AccountSecurityViewModelTest : BaseViewModelTest() { } @Test - fun `on DeleteAccountClick should emit ShowToast`() = runTest { + fun `on DeleteAccountClick should emit NavigateToDeleteAccount`() = runTest { val viewModel = createViewModel() viewModel.eventFlow.test { viewModel.trySendAction(AccountSecurityAction.DeleteAccountClick) - assertEquals( - AccountSecurityEvent.ShowToast("Not yet implemented.".asText()), - awaitItem(), - ) + assertEquals(AccountSecurityEvent.NavigateToDeleteAccount, awaitItem()) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt new file mode 100644 index 000000000..27cfcca89 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountScreenTest.kt @@ -0,0 +1,40 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount + +import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class DeleteAccountScreenTest : BaseComposeTest() { + + private var onNavigateBackCalled = false + + private val mutableEventFlow = MutableSharedFlow( + extraBufferCapacity = Int.MAX_VALUE, + ) + private val mutableStateFlow = MutableStateFlow(Unit) + private val viewModel = mockk(relaxed = true) { + every { eventFlow } returns mutableEventFlow + every { stateFlow } returns mutableStateFlow + } + + @Before + fun setUp() { + composeTestRule.setContent { + DeleteAccountScreen( + onNavigateBack = { onNavigateBackCalled = true }, + viewModel = viewModel, + ) + } + } + + @Test + fun `on NavigateBack should call onNavigateBack`() { + mutableEventFlow.tryEmit(DeleteAccountEvent.NavigateBack) + assertTrue(onNavigateBackCalled) + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt new file mode 100644 index 000000000..c65be0ffb --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/accountsecurity/deleteaccount/DeleteAccountViewModelTest.kt @@ -0,0 +1,43 @@ +package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.deleteaccount + +import app.cash.turbine.test +import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest +import com.x8bit.bitwarden.ui.platform.base.util.asText +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class DeleteAccountViewModelTest : BaseViewModelTest() { + + @Test + fun `on CancelClick should emit NavigateBack`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(DeleteAccountAction.CancelClick) + assertEquals(DeleteAccountEvent.NavigateBack, awaitItem()) + } + } + + @Test + fun `on CloseClick should emit NavigateBack`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(DeleteAccountAction.CloseClick) + assertEquals(DeleteAccountEvent.NavigateBack, awaitItem()) + } + } + + @Test + fun `on DeleteAccountClick should emit ShowToast`() = runTest { + val viewModel = createViewModel() + viewModel.eventFlow.test { + viewModel.trySendAction(DeleteAccountAction.DeleteAccountClick) + assertEquals( + DeleteAccountEvent.ShowToast("Not yet implemented.".asText()), + awaitItem(), + ) + } + } + + private fun createViewModel(): DeleteAccountViewModel = DeleteAccountViewModel() +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index 6bc12b775..7c8da3050 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -34,6 +34,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } onNodeWithText("My vault").performClick() @@ -56,6 +57,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } @@ -79,6 +81,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } onNodeWithText("Send").performClick() @@ -101,6 +104,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } @@ -124,6 +128,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } onNodeWithText("Generator").performClick() @@ -146,6 +151,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault") } @@ -169,6 +175,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } onNodeWithText("Settings").performClick() @@ -191,6 +198,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { navController = fakeNavHostController, onNavigateToVaultAddItem = {}, onNavigateToNewSend = {}, + onNavigateToDeleteAccount = {}, ) } runOnIdle { fakeNavHostController.assertCurrentRoute("vault") }