From 266db5cc04c4feeac87bd973381ecb3f23b51593 Mon Sep 17 00:00:00 2001 From: Brian Yencho Date: Fri, 8 Dec 2023 10:22:42 -0600 Subject: [PATCH] Bit 1207 add lock and logout to switcher (#353) --- .../ui/auth/feature/landing/LandingScreen.kt | 13 ++- .../ui/auth/feature/login/LoginScreen.kt | 11 ++- .../feature/vaultunlock/VaultUnlockScreen.kt | 11 ++- .../ui/platform/base/util/ToastUtils.kt | 19 ++++ .../components/BitwardenAccountSwitcher.kt | 98 ++++++++++++++++--- .../components/BitwardenBasicDialogRow.kt | 44 +++++++++ .../ui/vault/feature/vault/VaultScreen.kt | 12 ++- 7 files changed, 192 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ToastUtils.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenBasicDialogRow.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt index 4863719860..e6e3fcfe8f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/landing/LandingScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter 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.semantics.semantics @@ -48,6 +49,7 @@ import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.platform.repository.model.Environment import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog @@ -178,12 +180,21 @@ fun LandingScreen( .fillMaxSize(), ) + val context = LocalContext.current BitwardenAccountSwitcher( isVisible = isAccountMenuVisible, accountSummaries = state.accountSummaries.toImmutableList(), - onAccountSummaryClick = remember(viewModel) { + onSwitchAccountClick = remember(viewModel) { { viewModel.trySendAction(LandingAction.SwitchAccountClick(it)) } }, + onLockAccountClick = { + // TODO: Implement lock functionality (BIT-1207) + showNotYetImplementedToast(context) + }, + onLogoutAccountClick = { + // TODO: Implement logout functionality (BIT-1207) + showNotYetImplementedToast(context) + }, onAddAccountClick = { // Not available }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt index dde6611c16..55eec449ef 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/login/LoginScreen.kt @@ -39,6 +39,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler +import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog import com.x8bit.bitwarden.ui.platform.components.BitwardenFilledButton @@ -143,9 +144,17 @@ fun LoginScreen( BitwardenAccountSwitcher( isVisible = isAccountMenuVisible, accountSummaries = state.accountSummaries.toImmutableList(), - onAccountSummaryClick = remember(viewModel) { + onSwitchAccountClick = remember(viewModel) { { viewModel.trySendAction(LoginAction.SwitchAccountClick(it)) } }, + onLockAccountClick = { + // TODO: Implement lock functionality (BIT-1207) + showNotYetImplementedToast(context) + }, + onLogoutAccountClick = { + // TODO: Implement logout functionality (BIT-1207) + showNotYetImplementedToast(context) + }, onAddAccountClick = { // Not available }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index c2a8c6f6bf..d969e45252 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -32,6 +32,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect import com.x8bit.bitwarden.ui.platform.base.util.asText +import com.x8bit.bitwarden.ui.platform.base.util.showNotYetImplementedToast import com.x8bit.bitwarden.ui.platform.components.BasicDialogState import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher @@ -193,9 +194,17 @@ fun VaultUnlockScreen( BitwardenAccountSwitcher( isVisible = accountMenuVisible, accountSummaries = state.accountSummaries.toImmutableList(), - onAccountSummaryClick = remember(viewModel) { + onSwitchAccountClick = remember(viewModel) { { viewModel.trySendAction(VaultUnlockAction.SwitchAccountClick(it)) } }, + onLockAccountClick = { + // TODO: Implement lock functionality (BIT-1207) + showNotYetImplementedToast(context) + }, + onLogoutAccountClick = { + // TODO: Implement logout functionality (BIT-1207) + showNotYetImplementedToast(context) + }, onAddAccountClick = remember(viewModel) { { viewModel.trySendAction(VaultUnlockAction.AddAccountClick) } }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ToastUtils.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ToastUtils.kt new file mode 100644 index 0000000000..4acafd759a --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ToastUtils.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.ui.platform.base.util + +import android.content.Context +import android.widget.Toast +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage + +/** + * Shows a [Toast] with a message indicating something is not yet implemented. + */ +@OmitFromCoverage +fun showNotYetImplementedToast(context: Context) { + Toast + .makeText( + context, + "Not yet implemented", + Toast.LENGTH_SHORT, + ) + .show() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt index a664db5593..2f9fe79d2a 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt @@ -1,9 +1,13 @@ package com.x8bit.bitwarden.ui.platform.components import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.updateTransition import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,7 +29,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -62,7 +69,12 @@ private const val MAXIMUM_ACCOUNT_LIMIT = 5 * @param isVisible Whether or not this component is visible. Changing this value will animate the * component in or out of view. * @param accountSummaries The accounts to display in the switcher. - * @param onAccountSummaryClick A callback when an account is clicked. + * @param onSwitchAccountClick A callback when an account is clicked indicating that the account + * should be switched to. + * @param onLockAccountClick A callback when an account is clicked indicating that the account + * should be locked. + * @param onLogoutAccountClick A callback when an account is clicked indicating that the account + * should be logged out. * @param onAddAccountClick A callback when the Add Account row is clicked. * @param onDismissRequest A callback when the component requests to be dismissed. This is triggered * whenever the user clicks on the scrim or any of the switcher items. @@ -78,13 +90,29 @@ private const val MAXIMUM_ACCOUNT_LIMIT = 5 fun BitwardenAccountSwitcher( isVisible: Boolean, accountSummaries: ImmutableList, - onAccountSummaryClick: (AccountSummary) -> Unit, + onSwitchAccountClick: (AccountSummary) -> Unit, + onLockAccountClick: (AccountSummary) -> Unit, + onLogoutAccountClick: (AccountSummary) -> Unit, onAddAccountClick: () -> Unit, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, isAddAccountAvailable: Boolean = true, topAppBarScrollBehavior: TopAppBarScrollBehavior, ) { + // Track the actual visibility (according to the internal transitions) so that we know when we + // can safely show dialogs. + var isVisibleActual by remember { mutableStateOf(isVisible) } + + var lockOrLogoutAccount by remember { mutableStateOf(null) } + if (lockOrLogoutAccount != null && !isVisibleActual) { + LockOrLogoutDialog( + accountSummary = requireNotNull(lockOrLogoutAccount), + onDismissRequest = { lockOrLogoutAccount = null }, + onLockAccountClick = onLockAccountClick, + onLogoutAccountClick = onLogoutAccountClick, + ) + } + Box(modifier = modifier) { BitwardenAnimatedScrim( isVisible = isVisible, @@ -95,9 +123,13 @@ fun BitwardenAccountSwitcher( AnimatedAccountSwitcher( isVisible = isVisible, accountSummaries = accountSummaries, - onAccountSummaryClick = { + onSwitchAccountClick = { onDismissRequest() - onAccountSummaryClick(it) + onSwitchAccountClick(it) + }, + onSwitchAccountLongClick = { + onDismissRequest() + lockOrLogoutAccount = it }, onAddAccountClick = { onDismissRequest() @@ -105,27 +137,38 @@ fun BitwardenAccountSwitcher( }, isAddAccountAvailable = isAddAccountAvailable, topAppBarScrollBehavior = topAppBarScrollBehavior, + currentAnimationState = { isVisibleActual = it }, modifier = Modifier .fillMaxWidth(), ) } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalAnimationApi::class, +) @Composable private fun AnimatedAccountSwitcher( isVisible: Boolean, accountSummaries: ImmutableList, - onAccountSummaryClick: (AccountSummary) -> Unit, + onSwitchAccountClick: (AccountSummary) -> Unit, + onSwitchAccountLongClick: (AccountSummary) -> Unit, onAddAccountClick: () -> Unit, isAddAccountAvailable: Boolean, modifier: Modifier = Modifier, topAppBarScrollBehavior: TopAppBarScrollBehavior, + currentAnimationState: (isVisible: Boolean) -> Unit, ) { val expandedColor = MaterialTheme.colorScheme.surface val collapsedColor = MaterialTheme.colorScheme.surfaceContainer - AnimatedVisibility( - visible = isVisible, + val transition = updateTransition( + targetState = isVisible, + label = "AnimatedAccountSwitcher", + ) + .also { currentAnimationState(it.currentState) } + transition.AnimatedVisibility( + visible = { it }, enter = slideInVertically { -it }, exit = slideOutVertically { -it }, ) { @@ -153,7 +196,8 @@ private fun AnimatedAccountSwitcher( items(accountSummaries) { accountSummary -> AccountSummaryItem( accountSummary = accountSummary, - onClick = onAccountSummaryClick, + onSwitchAccountClick = onSwitchAccountClick, + onSwitchAccountLongClick = onSwitchAccountLongClick, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), @@ -177,20 +221,23 @@ private fun AnimatedAccountSwitcher( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun AccountSummaryItem( accountSummary: AccountSummary, - onClick: (AccountSummary) -> Unit, + onSwitchAccountClick: (AccountSummary) -> Unit, + onSwitchAccountLongClick: (AccountSummary) -> Unit, modifier: Modifier = Modifier, ) { Row( horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .clickable( + .combinedClickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(color = MaterialTheme.colorScheme.primary), - onClick = { onClick(accountSummary) }, + onClick = { onSwitchAccountClick(accountSummary) }, + onLongClick = { onSwitchAccountLongClick(accountSummary) }, ) .padding(vertical = 8.dp) .then(modifier), @@ -248,6 +295,33 @@ private fun AccountSummaryItem( } } +@Composable +private fun LockOrLogoutDialog( + accountSummary: AccountSummary, + onDismissRequest: () -> Unit, + onLockAccountClick: (AccountSummary) -> Unit, + onLogoutAccountClick: (AccountSummary) -> Unit, +) { + BitwardenSelectionDialog( + title = "${accountSummary.email}\n${accountSummary.environmentLabel}", + onDismissRequest = onDismissRequest, + selectionItems = { + BitwardenBasicDialogRow( + text = stringResource(id = R.string.lock), + onClick = { + onLockAccountClick(accountSummary) + }, + ) + BitwardenBasicDialogRow( + text = stringResource(id = R.string.log_out), + onClick = { + onLogoutAccountClick(accountSummary) + }, + ) + }, + ) +} + @Composable private fun AddAccountItem( onClick: () -> Unit, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenBasicDialogRow.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenBasicDialogRow.kt new file mode 100644 index 0000000000..4c0be3ef75 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenBasicDialogRow.kt @@ -0,0 +1,44 @@ +package com.x8bit.bitwarden.ui.platform.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * A simple clickable row for use within a [BitwardenSelectionDialog] as an alternative to a + * [BitwardenSelectionRow]. + * + * @param text The text to display in the row. + * @param onClick A callback to be invoked when the row is clicked. + * @param modifier A [Modifier] for the composable. + */ +@Composable +fun BitwardenBasicDialogRow( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + modifier = modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + onClick = onClick, + ) + .padding( + vertical = 16.dp, + horizontal = 24.dp, + ) + .fillMaxWidth(), + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 221d0f44dd..3ccb7d97c2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -28,6 +28,7 @@ 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.base.util.showNotYetImplementedToast import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher import com.x8bit.bitwarden.ui.platform.components.BitwardenMediumTopAppBar @@ -213,10 +214,19 @@ private fun VaultScreenScaffold( ) } + val context = LocalContext.current BitwardenAccountSwitcher( isVisible = accountMenuVisible, accountSummaries = state.accountSummaries.toImmutableList(), - onAccountSummaryClick = accountSwitchClickAction, + onSwitchAccountClick = accountSwitchClickAction, + onLockAccountClick = { + // TODO: Implement lock functionality (BIT-1207) + showNotYetImplementedToast(context) + }, + onLogoutAccountClick = { + // TODO: Implement logout functionality (BIT-1207) + showNotYetImplementedToast(context) + }, onAddAccountClick = addAccountClickAction, onDismissRequest = { updateAccountMenuVisibility(false) }, topAppBarScrollBehavior = scrollBehavior,