Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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.navigationBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
Expand All @@ -18,7 +19,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException
import androidx.credentials.providerevents.exception.ImportCredentialsCancellationException
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
Expand All @@ -27,7 +28,8 @@ import com.bitwarden.cxf.ui.composition.LocalCredentialExchangeCompletionManager
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.content.BitwardenEmptyContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
Expand All @@ -36,13 +38,15 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.component.AccountSummary
import com.x8bit.bitwarden.ui.vault.feature.exportitems.component.ExportItemsScaffold
import com.x8bit.bitwarden.ui.vault.feature.exportitems.model.AccountSelectionListItem
import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.handlers.rememberSelectAccountHandlers
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

/**
* Top level screen for selecting an account to export items from.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Suppress("LongMethod")
fun SelectAccountScreen(
onAccountSelected: (userId: String) -> Unit,
viewModel: SelectAccountViewModel = hiltViewModel(),
Expand All @@ -57,9 +61,7 @@ fun SelectAccountScreen(
credentialExchangeCompletionManager
.completeCredentialExport(
exportResult = ExportCredentialsResult.Failure(
// TODO: [PM-26094] Use ImportCredentialsCancellationException once
// public.
error = ImportCredentialsUnknownErrorException(
error = ImportCredentialsCancellationException(
errorMessage = "User cancelled import.",
),
),
Expand All @@ -82,19 +84,40 @@ fun SelectAccountScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
SelectAccountContent(
state = state,
onAccountClick = handlers.onAccountClick,
modifier = Modifier
.fillMaxSize()
.standardHorizontalMargin(),
)
when (val viewState = state.viewState) {
is SelectAccountState.ViewState.Content -> {
SelectAccountContent(
accountSelectionListItems = viewState.accountSelectionListItems,
onAccountClick = handlers.onAccountClick,
modifier = Modifier.fillMaxSize(),
)
}

SelectAccountState.ViewState.Loading -> {
BitwardenLoadingContent(
text = stringResource(BitwardenString.loading),
modifier = Modifier.fillMaxSize(),
)
}

SelectAccountState.ViewState.NoItems -> {
BitwardenEmptyContent(
title = stringResource(BitwardenString.no_accounts_available),
titleTestTag = "NoAccountsTitle",
text = stringResource(
BitwardenString.you_dont_have_any_accounts_you_can_import_from,
),
labelTestTag = "NoAccountsText",
modifier = Modifier.fillMaxSize(),
)
}
}
}
}

@Composable
private fun SelectAccountContent(
state: SelectAccountState,
accountSelectionListItems: ImmutableList<AccountSelectionListItem>,
onAccountClick: (userId: String) -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -106,62 +129,121 @@ private fun SelectAccountContent(
text = stringResource(BitwardenString.select_account),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.titleMedium,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}

item { Spacer(Modifier.height(16.dp)) }

itemsIndexed(
items = state.accountSelectionListItems,
items = accountSelectionListItems,
key = { _, item -> "AccountSummaryItem_${item.userId}" },
) { index, item ->
AccountSummaryListItem(
item = item,
cardStyle = state.accountSelectionListItems.toListItemCardStyle(index),
cardStyle = accountSelectionListItems.toListItemCardStyle(index),
clickable = true,
onClick = onAccountClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
item { Spacer(Modifier.height(16.dp)) }
item { Spacer(Modifier.navigationBarsPadding()) }
}
}

@Preview(showBackground = true)
@OptIn(ExperimentalMaterial3Api::class)
@Preview(
showBackground = true,
name = "Select account content",
showSystemUi = true,
)
@Composable
private fun SelectAccountContentPreview() {
val state = SelectAccountState(
accountSelectionListItems = persistentListOf(
AccountSelectionListItem(
userId = "1",
email = "john.doe@example.com",
initials = "JD",
avatarColorHex = "#FFFF0000",
isItemRestricted = false,
),
AccountSelectionListItem(
userId = "2",
email = "jane.smith@example.com",
initials = "JS",
avatarColorHex = "#FF00FF00",
isItemRestricted = true,
),
AccountSelectionListItem(
userId = "3",
email = "another.user@example.com",
initials = "AU",
avatarColorHex = "#FF0000FF",
isItemRestricted = false,
),
),
)
BitwardenScaffold {
private fun SelectAccountContent_preview() {
ExportItemsScaffold(
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(BitwardenString.close),
onNavigationIconClick = { },
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
) {
SelectAccountContent(
state = state,
accountSelectionListItems = persistentListOf(
AccountSelectionListItem(
userId = "1",
email = "john.doe@example.com",
initials = "JD",
avatarColorHex = "#FFFF0000",
isItemRestricted = false,
),
AccountSelectionListItem(
userId = "2",
email = "jane.smith@example.com",
initials = "JS",
avatarColorHex = "#FF00FF00",
isItemRestricted = true,
),
AccountSelectionListItem(
userId = "3",
email = "another.user@example.com",
initials = "AU",
avatarColorHex = "#FF0000FF",
isItemRestricted = false,
),
),
onAccountClick = { },
modifier = Modifier.fillMaxSize(),
)
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Preview(
showBackground = true,
name = "No accounts content",
showSystemUi = true,
)
@Composable
private fun NoAccountsContent_preview() {
ExportItemsScaffold(
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(BitwardenString.close),
onNavigationIconClick = { },
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
) {
BitwardenEmptyContent(
title = stringResource(BitwardenString.no_accounts_available),
titleTestTag = "NoAccountsTitle",
text = stringResource(
BitwardenString.you_dont_have_any_accounts_you_can_import_from,
),
labelTestTag = "NoAccountsText",
modifier = Modifier.fillMaxSize(),
)
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Preview(
showBackground = true,
name = "Loading content",
showSystemUi = true,
)
@Composable
private fun LoadingContent_preview() {
ExportItemsScaffold(
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(BitwardenString.close),
onNavigationIconClick = { },
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
) {
BitwardenLoadingContent(
text = stringResource(BitwardenString.loading),
modifier = Modifier.fillMaxSize(),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount

import android.os.Parcelable
import androidx.lifecycle.viewModelScope
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.network.model.SyncResponseJson
Expand All @@ -11,12 +12,13 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.model.AccountSelectionLi
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import javax.inject.Inject

/**
Expand All @@ -28,7 +30,7 @@ class SelectAccountViewModel @Inject constructor(
policyManager: PolicyManager,
) : BaseViewModel<SelectAccountState, SelectAccountEvent, SelectAccountAction>(
initialState = SelectAccountState(
accountSelectionListItems = persistentListOf(),
viewState = SelectAccountState.ViewState.Loading,
),
) {

Expand Down Expand Up @@ -95,43 +97,79 @@ class SelectAccountViewModel @Inject constructor(
.filter { it.isEnabled }
.map { it.organizationId }

val accountSelectionListItems = action.userState
?.accounts
.orEmpty()
// We only want accounts that do not restrict personal vault ownership
.filter { account ->
account
.organizations
.none { org -> org.id in personalOwnershipRestrictedOrgIds }
}
.map { account ->
AccountSelectionListItem(
userId = account.userId,
email = account.email,
initials = account.initials,
avatarColorHex = account.avatarColorHex,
// Indicate which accounts have item restrictions applied.
isItemRestricted = account
.organizations
.any { org -> org.id in itemRestrictedOrgIds },
)
}
.toImmutableList()

mutableStateFlow.update {
it.copy(
accountSelectionListItems = action.userState
?.accounts
.orEmpty()
// We only want accounts that do not restrict personal vault ownership
.filter { account ->
account
.organizations
.none { org -> org.id in personalOwnershipRestrictedOrgIds }
}
.map { account ->
AccountSelectionListItem(
userId = account.userId,
email = account.email,
initials = account.initials,
avatarColorHex = account.avatarColorHex,
// Indicate which accounts have item restrictions applied.
isItemRestricted = account
.organizations
.any { org -> org.id in itemRestrictedOrgIds },
)
}
.toImmutableList(),
viewState = if (accountSelectionListItems.isEmpty()) {
SelectAccountState.ViewState.NoItems
} else {
SelectAccountState.ViewState.Content(
accountSelectionListItems = accountSelectionListItems,
)
},
)
}
}
}

/**
* Represents the state for the select account screen.
*
* @param accountSelectionListItems The list of account summaries to be displayed for selection.
*/
@Parcelize
@Serializable
data class SelectAccountState(
val accountSelectionListItems: ImmutableList<AccountSelectionListItem>,
)
val viewState: ViewState,
) : Parcelable {

/**
* Represents the different states for the select account screen.
*/
@Parcelize
@Serializable
sealed class ViewState : Parcelable {
/**
* Represents the loading state for the select account screen.
*/
data object Loading : ViewState()

/**
* Represents the content state for the select account screen.
*
* @param accountSelectionListItems The list of account summaries to be displayed for
* selection.
*/
data class Content(
val accountSelectionListItems: ImmutableList<AccountSelectionListItem>,
) : ViewState()

/**
* Represents the no items state for the select account screen.
*/
data object NoItems : ViewState()
}
}

/**
* Represents the actions that can be performed on the select account screen.
Expand Down
Loading