Update Vault Item Listing screen app bar for autofill (#834)

This commit is contained in:
Brian Yencho
2024-01-28 16:34:36 -06:00
committed by Álison Fernandes
parent 0a6b0f8dc7
commit 5b854c17b7
12 changed files with 537 additions and 22 deletions

View File

@@ -10,7 +10,7 @@ import kotlinx.parcelize.Parcelize
* @property uri A URI representing the location where data should be filled (if available).
*/
@Parcelize
class AutofillSelectionData(
data class AutofillSelectionData(
val type: Type,
val uri: String?,
) : Parcelable {

View File

@@ -12,6 +12,7 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.core.graphics.toColorInt
import java.net.URI
import java.net.URISyntaxException
import java.text.Normalizer
import java.util.Locale
import kotlin.math.floor
@@ -54,6 +55,19 @@ fun String.isValidUri(): Boolean =
false
}
/**
* Returns the host name (or path as a fallback) for the given [String] if it represents a
* well-formed URI, or `null` otherwise.
*/
fun String.toHostOrPathOrNull(): String? {
val uri = try {
URI(this)
} catch (e: URISyntaxException) {
return null
}
return uri.host ?: uri.path
}
/**
* Returns the original [String] only if:
*

View File

@@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
@@ -92,6 +93,9 @@ fun BitwardenTopAppBar(
Text(
text = title,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
},
actions = actions,

View File

@@ -67,6 +67,7 @@ class RootNavViewModel @Inject constructor(
when (specialCircumstance) {
is SpecialCircumstance.AutofillSelection -> {
RootNavState.VaultUnlockedForAutofillSelection(
activeUserId = userState.activeAccount.userId,
type = specialCircumstance.autofillSelectionData.type,
)
}
@@ -122,6 +123,7 @@ sealed class RootNavState : Parcelable {
*/
@Parcelize
data class VaultUnlockedForAutofillSelection(
val activeUserId: String,
val type: AutofillSelectionData.Type,
) : RootNavState()

View File

@@ -15,7 +15,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -26,6 +29,8 @@ 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.BasicDialogState
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenAccountSwitcher
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
@@ -35,12 +40,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
* Displays the vault item listing screen.
@@ -50,7 +58,7 @@ import kotlinx.collections.immutable.persistentListOf
fun VaultItemListingScreen(
onNavigateBack: () -> Unit,
onNavigateToVaultItem: (id: String) -> Unit,
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
onNavigateToVaultEditItemScreen: (cipherVaultId: String) -> Unit,
onNavigateToVaultAddItemScreen: () -> Unit,
onNavigateToAddSendItem: () -> Unit,
onNavigateToEditSendItem: (sendId: String) -> Unit,
@@ -158,6 +166,7 @@ private fun VaultItemListingScaffold(
pullToRefreshState: PullToRefreshState?,
vaultItemListingHandlers: VaultItemListingHandlers,
) {
var isAccountMenuVisible by rememberSaveable { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
@@ -165,28 +174,40 @@ private fun VaultItemListingScaffold(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = state.itemListingType.titleText(),
title = state.appBarTitle(),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = vaultItemListingHandlers.backClick,
navigationIcon = NavigationIcon(
navigationIcon = painterResource(id = R.drawable.ic_back),
navigationIconContentDescription = stringResource(id = R.string.back),
onNavigationIconClick = vaultItemListingHandlers.backClick,
)
.takeIf { state.shouldShowNavigationIcon },
actions = {
if (state.shouldShowAccountSwitcher) {
BitwardenAccountActionItem(
initials = state.activeAccountSummary.initials,
color = state.activeAccountSummary.avatarColor,
onClick = { isAccountMenuVisible = !isAccountMenuVisible },
)
}
BitwardenSearchActionItem(
contentDescription = stringResource(id = R.string.search_vault),
onClick = vaultItemListingHandlers.searchIconClick,
)
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
OverflowMenuItemData(
text = stringResource(id = R.string.sync),
onClick = vaultItemListingHandlers.syncClick,
if (state.shouldShowOverflowMenu) {
BitwardenOverflowActionItem(
menuItemDataList = persistentListOf(
OverflowMenuItemData(
text = stringResource(id = R.string.sync),
onClick = vaultItemListingHandlers.syncClick,
),
OverflowMenuItemData(
text = stringResource(id = R.string.lock),
onClick = vaultItemListingHandlers.lockClick,
),
),
OverflowMenuItemData(
text = stringResource(id = R.string.lock),
onClick = vaultItemListingHandlers.lockClick,
),
),
)
)
}
},
)
},
@@ -239,5 +260,20 @@ private fun VaultItemListingScaffold(
BitwardenLoadingContent(modifier = modifier)
}
}
BitwardenAccountSwitcher(
isVisible = isAccountMenuVisible,
accountSummaries = state.accountSummaries.toImmutableList(),
onSwitchAccountClick = vaultItemListingHandlers.switchAccountClick,
onLockAccountClick = vaultItemListingHandlers.lockAccountClick,
onLogoutAccountClick = vaultItemListingHandlers.logoutAccountClick,
onAddAccountClick = {
// Not available
},
onDismissRequest = { isAccountMenuVisible = false },
isAddAccountAvailable = false,
topAppBarScrollBehavior = scrollBehavior,
modifier = modifier,
)
}
}

View File

@@ -4,6 +4,7 @@ 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.data.autofill.manager.AutofillSelectionManager
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
@@ -22,6 +23,8 @@ 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 com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
@@ -32,6 +35,8 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toSearchType
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toViewState
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.updateWithAdditionalDataIfNecessary
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
@@ -53,6 +58,7 @@ class VaultItemListingViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val clock: Clock,
private val clipboardManager: BitwardenClipboardManager,
private val authRepository: AuthRepository,
private val vaultRepository: VaultRepository,
private val environmentRepository: EnvironmentRepository,
private val settingsRepository: SettingsRepository,
@@ -60,12 +66,17 @@ class VaultItemListingViewModel @Inject constructor(
private val specialCircumstanceManager: SpecialCircumstanceManager,
) : BaseViewModel<VaultItemListingState, VaultItemListingEvent, VaultItemListingsAction>(
initialState = run {
val userState = requireNotNull(authRepository.userStateFlow.value)
val activeAccountSummary = userState.toActiveAccountSummary()
val accountSummaries = userState.toAccountSummaries()
val specialCircumstance =
specialCircumstanceManager.specialCircumstance as? SpecialCircumstance.AutofillSelection
VaultItemListingState(
itemListingType = VaultItemListingArgs(savedStateHandle = savedStateHandle)
.vaultItemListingType
.toItemListingType(),
activeAccountSummary = activeAccountSummary,
accountSummaries = accountSummaries,
viewState = VaultItemListingState.ViewState.Loading,
vaultFilterType = vaultRepository.vaultFilterType,
baseWebSendUrl = environmentRepository.environment.environmentUrlData.baseWebSendUrl,
@@ -99,6 +110,9 @@ class VaultItemListingViewModel @Inject constructor(
override fun handleAction(action: VaultItemListingsAction) {
when (action) {
is VaultItemListingsAction.LockAccountClick -> handleLockAccountClick(action)
is VaultItemListingsAction.LogoutAccountClick -> handleLogoutAccountClick(action)
is VaultItemListingsAction.SwitchAccountClick -> handleSwitchAccountClick(action)
is VaultItemListingsAction.DismissDialogClick -> handleDismissDialogClick()
is VaultItemListingsAction.BackClick -> handleBackClick()
is VaultItemListingsAction.LockClick -> handleLockClick()
@@ -114,6 +128,18 @@ class VaultItemListingViewModel @Inject constructor(
}
//region VaultItemListing Handlers
private fun handleLockAccountClick(action: VaultItemListingsAction.LockAccountClick) {
vaultRepository.lockVault(userId = action.accountSummary.userId)
}
private fun handleLogoutAccountClick(action: VaultItemListingsAction.LogoutAccountClick) {
authRepository.logout(userId = action.accountSummary.userId)
}
private fun handleSwitchAccountClick(action: VaultItemListingsAction.SwitchAccountClick) {
authRepository.switchAccount(userId = action.accountSummary.userId)
}
private fun handleRefreshClick() {
vaultRepository.sync()
}
@@ -414,6 +440,12 @@ class VaultItemListingViewModel @Inject constructor(
private fun handleVaultDataReceive(
action: VaultItemListingsAction.Internal.VaultDataReceive,
) {
if (state.activeAccountSummary.userId != authRepository.userStateFlow.value?.activeUserId) {
// We are in the process of switching accounts, so we should ignore any updates here
// to avoid any unnecessary visual changes.
return
}
when (val vaultData = action.vaultData) {
is DataState.Error -> vaultErrorReceive(vaultData = vaultData)
is DataState.Loaded -> vaultLoadedReceive(vaultData = vaultData)
@@ -541,6 +573,8 @@ class VaultItemListingViewModel @Inject constructor(
*/
data class VaultItemListingState(
val itemListingType: ItemListingType,
val activeAccountSummary: AccountSummary,
val accountSummaries: List<AccountSummary>,
val viewState: ViewState,
val vaultFilterType: VaultFilterType,
val baseWebSendUrl: String,
@@ -549,9 +583,24 @@ data class VaultItemListingState(
val dialogState: DialogState?,
// Internal
private val isPullToRefreshSettingEnabled: Boolean,
private val autofillSelectionData: AutofillSelectionData? = null,
private val shouldFinishOnComplete: Boolean = false,
val autofillSelectionData: AutofillSelectionData? = null,
val shouldFinishOnComplete: Boolean = false,
) {
/**
* Whether or not this represents a listing screen for autofill.
*/
val isAutofill: Boolean
get() = autofillSelectionData != null
/**
* A displayable title for the AppBar.
*/
val appBarTitle: Text
get() = autofillSelectionData
?.uri
?.toHostOrPathOrNull()
?.let { R.string.items_for_uri.asText(it) }
?: itemListingType.titleText
/**
* Indicates that the pull-to-refresh should be enabled in the UI.
@@ -560,10 +609,19 @@ data class VaultItemListingState(
get() = isPullToRefreshSettingEnabled && viewState.isPullToRefreshEnabled
/**
* Whether or not this represents a listing screen for autofill.
* Whether or not the account switcher should be shown.
*/
val isAutofill: Boolean
get() = autofillSelectionData != null
val shouldShowAccountSwitcher: Boolean get() = isAutofill
/**
* Whether or not the navigation icon should be shown.
*/
val shouldShowNavigationIcon: Boolean get() = !isAutofill
/**
* Whether or not the overflow menu should be shown.
*/
val shouldShowOverflowMenu: Boolean get() = !isAutofill
/**
* Represents the current state of any dialogs on the screen.
@@ -843,6 +901,28 @@ sealed class VaultItemListingEvent {
* Models actions for the [VaultItemListingScreen].
*/
sealed class VaultItemListingsAction {
/**
* Indicates the user has clicked on the given [accountSummary] information in order to lock
* the associated account's vault.
*/
data class LockAccountClick(
val accountSummary: AccountSummary,
) : VaultItemListingsAction()
/**
* Indicates the user has clicked on the given [accountSummary] information in order to log out
* of that account.
*/
data class LogoutAccountClick(
val accountSummary: AccountSummary,
) : VaultItemListingsAction()
/**
* The user has clicked the an account to switch too.
*/
data class SwitchAccountClick(
val accountSummary: AccountSummary,
) : VaultItemListingsAction()
/**
* Click to dismiss the dialog.

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingViewModel
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingsAction
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
@@ -9,6 +10,9 @@ import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflo
* items.
*/
data class VaultItemListingHandlers(
val switchAccountClick: (AccountSummary) -> Unit,
val lockAccountClick: (AccountSummary) -> Unit,
val logoutAccountClick: (AccountSummary) -> Unit,
val backClick: () -> Unit,
val searchIconClick: () -> Unit,
val addVaultItemClick: () -> Unit,
@@ -27,6 +31,15 @@ data class VaultItemListingHandlers(
viewModel: VaultItemListingViewModel,
): VaultItemListingHandlers =
VaultItemListingHandlers(
switchAccountClick = {
viewModel.trySendAction(VaultItemListingsAction.SwitchAccountClick(it))
},
lockAccountClick = {
viewModel.trySendAction(VaultItemListingsAction.LockAccountClick(it))
},
logoutAccountClick = {
viewModel.trySendAction(VaultItemListingsAction.LogoutAccountClick(it))
},
backClick = { viewModel.trySendAction(VaultItemListingsAction.BackClick) },
searchIconClick = {
viewModel.trySendAction(VaultItemListingsAction.SearchIconClick)