mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 19:17:16 -06:00
[PM-26355] Improve SelectAccountScreen state handling (#5965)
This commit is contained in:
parent
2eb829a25b
commit
acc9113f9a
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@ -18,7 +19,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
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.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
|
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
|
||||||
@ -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.EventsEffect
|
||||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||||
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
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.components.util.rememberVectorPainter
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||||
@ -36,6 +38,7 @@ 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.component.ExportItemsScaffold
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.exportitems.model.AccountSelectionListItem
|
import com.x8bit.bitwarden.ui.vault.feature.exportitems.model.AccountSelectionListItem
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.handlers.rememberSelectAccountHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.handlers.rememberSelectAccountHandlers
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,6 +46,7 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
|
@Suppress("LongMethod")
|
||||||
fun SelectAccountScreen(
|
fun SelectAccountScreen(
|
||||||
onAccountSelected: (userId: String) -> Unit,
|
onAccountSelected: (userId: String) -> Unit,
|
||||||
viewModel: SelectAccountViewModel = hiltViewModel(),
|
viewModel: SelectAccountViewModel = hiltViewModel(),
|
||||||
@ -57,9 +61,7 @@ fun SelectAccountScreen(
|
|||||||
credentialExchangeCompletionManager
|
credentialExchangeCompletionManager
|
||||||
.completeCredentialExport(
|
.completeCredentialExport(
|
||||||
exportResult = ExportCredentialsResult.Failure(
|
exportResult = ExportCredentialsResult.Failure(
|
||||||
// TODO: [PM-26094] Use ImportCredentialsCancellationException once
|
error = ImportCredentialsCancellationException(
|
||||||
// public.
|
|
||||||
error = ImportCredentialsUnknownErrorException(
|
|
||||||
errorMessage = "User cancelled import.",
|
errorMessage = "User cancelled import.",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -82,19 +84,40 @@ fun SelectAccountScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) {
|
) {
|
||||||
SelectAccountContent(
|
when (val viewState = state.viewState) {
|
||||||
state = state,
|
is SelectAccountState.ViewState.Content -> {
|
||||||
onAccountClick = handlers.onAccountClick,
|
SelectAccountContent(
|
||||||
modifier = Modifier
|
accountSelectionListItems = viewState.accountSelectionListItems,
|
||||||
.fillMaxSize()
|
onAccountClick = handlers.onAccountClick,
|
||||||
.standardHorizontalMargin(),
|
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
|
@Composable
|
||||||
private fun SelectAccountContent(
|
private fun SelectAccountContent(
|
||||||
state: SelectAccountState,
|
accountSelectionListItems: ImmutableList<AccountSelectionListItem>,
|
||||||
onAccountClick: (userId: String) -> Unit,
|
onAccountClick: (userId: String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@ -106,62 +129,121 @@ private fun SelectAccountContent(
|
|||||||
text = stringResource(BitwardenString.select_account),
|
text = stringResource(BitwardenString.select_account),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
style = BitwardenTheme.typography.titleMedium,
|
style = BitwardenTheme.typography.titleMedium,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(16.dp)) }
|
item { Spacer(Modifier.height(16.dp)) }
|
||||||
|
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = state.accountSelectionListItems,
|
items = accountSelectionListItems,
|
||||||
key = { _, item -> "AccountSummaryItem_${item.userId}" },
|
key = { _, item -> "AccountSummaryItem_${item.userId}" },
|
||||||
) { index, item ->
|
) { index, item ->
|
||||||
AccountSummaryListItem(
|
AccountSummaryListItem(
|
||||||
item = item,
|
item = item,
|
||||||
cardStyle = state.accountSelectionListItems.toListItemCardStyle(index),
|
cardStyle = accountSelectionListItems.toListItemCardStyle(index),
|
||||||
clickable = true,
|
clickable = true,
|
||||||
onClick = onAccountClick,
|
onClick = onAccountClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin()
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { Spacer(Modifier.height(16.dp)) }
|
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
|
@Composable
|
||||||
private fun SelectAccountContentPreview() {
|
private fun SelectAccountContent_preview() {
|
||||||
val state = SelectAccountState(
|
ExportItemsScaffold(
|
||||||
accountSelectionListItems = persistentListOf(
|
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
|
||||||
AccountSelectionListItem(
|
navigationIconContentDescription = stringResource(BitwardenString.close),
|
||||||
userId = "1",
|
onNavigationIconClick = { },
|
||||||
email = "john.doe@example.com",
|
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||||
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 {
|
|
||||||
SelectAccountContent(
|
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 = { },
|
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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount
|
package com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitwarden.network.model.PolicyTypeJson
|
import com.bitwarden.network.model.PolicyTypeJson
|
||||||
import com.bitwarden.network.model.SyncResponseJson
|
import com.bitwarden.network.model.SyncResponseJson
|
||||||
@ -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 com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,7 +30,7 @@ class SelectAccountViewModel @Inject constructor(
|
|||||||
policyManager: PolicyManager,
|
policyManager: PolicyManager,
|
||||||
) : BaseViewModel<SelectAccountState, SelectAccountEvent, SelectAccountAction>(
|
) : BaseViewModel<SelectAccountState, SelectAccountEvent, SelectAccountAction>(
|
||||||
initialState = SelectAccountState(
|
initialState = SelectAccountState(
|
||||||
accountSelectionListItems = persistentListOf(),
|
viewState = SelectAccountState.ViewState.Loading,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -95,30 +97,38 @@ class SelectAccountViewModel @Inject constructor(
|
|||||||
.filter { it.isEnabled }
|
.filter { it.isEnabled }
|
||||||
.map { it.organizationId }
|
.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 {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
accountSelectionListItems = action.userState
|
viewState = if (accountSelectionListItems.isEmpty()) {
|
||||||
?.accounts
|
SelectAccountState.ViewState.NoItems
|
||||||
.orEmpty()
|
} else {
|
||||||
// We only want accounts that do not restrict personal vault ownership
|
SelectAccountState.ViewState.Content(
|
||||||
.filter { account ->
|
accountSelectionListItems = accountSelectionListItems,
|
||||||
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(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,12 +136,40 @@ class SelectAccountViewModel @Inject constructor(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the state for the select account screen.
|
* 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(
|
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.
|
* Represents the actions that can be performed on the select account screen.
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.exportitems
|
package com.x8bit.bitwarden.ui.vault.feature.exportitems
|
||||||
|
|
||||||
import androidx.compose.ui.test.isDisplayed
|
import androidx.compose.ui.test.isDisplayed
|
||||||
|
import androidx.compose.ui.test.isNotDisplayed
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
@ -124,6 +125,42 @@ class SelectAccountScreenTest : BitwardenComposeTest() {
|
|||||||
|
|
||||||
assertTrue(onAccountSelectedCalled)
|
assertTrue(onAccountSelectedCalled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NoItemsContent should be displayed according to state`() = runTest {
|
||||||
|
mockkStateFlow.emit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = SelectAccountState.ViewState.NoItems,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("No accounts available")
|
||||||
|
.isDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText(
|
||||||
|
text = "You don't have any accounts you can import from.",
|
||||||
|
substring = true,
|
||||||
|
)
|
||||||
|
.isDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Select an account")
|
||||||
|
.isNotDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Loading content should be displayed according to state`() = runTest {
|
||||||
|
mockkStateFlow.emit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = SelectAccountState.ViewState.Loading,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Loading")
|
||||||
|
.isDisplayed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||||
@ -148,20 +185,22 @@ private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
|
|||||||
)
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE = SelectAccountState(
|
private val DEFAULT_STATE = SelectAccountState(
|
||||||
accountSelectionListItems = persistentListOf(
|
viewState = SelectAccountState.ViewState.Content(
|
||||||
AccountSelectionListItem(
|
accountSelectionListItems = persistentListOf(
|
||||||
userId = ACTIVE_ACCOUNT_SUMMARY.userId,
|
AccountSelectionListItem(
|
||||||
email = ACTIVE_ACCOUNT_SUMMARY.email,
|
userId = ACTIVE_ACCOUNT_SUMMARY.userId,
|
||||||
initials = "AA",
|
email = ACTIVE_ACCOUNT_SUMMARY.email,
|
||||||
avatarColorHex = "#FFFF0000",
|
initials = "AA",
|
||||||
isItemRestricted = false,
|
avatarColorHex = "#FFFF0000",
|
||||||
),
|
isItemRestricted = false,
|
||||||
AccountSelectionListItem(
|
),
|
||||||
userId = LOCKED_ACCOUNT_SUMMARY.userId,
|
AccountSelectionListItem(
|
||||||
email = LOCKED_ACCOUNT_SUMMARY.email,
|
userId = LOCKED_ACCOUNT_SUMMARY.userId,
|
||||||
initials = "LU",
|
email = LOCKED_ACCOUNT_SUMMARY.email,
|
||||||
avatarColorHex = "#FF00FF00",
|
initials = "LU",
|
||||||
isItemRestricted = false,
|
avatarColorHex = "#FF00FF00",
|
||||||
|
isItemRestricted = false,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -50,11 +50,8 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `initial state should be correct`() = runTest {
|
fun `initial state should be correct`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SelectAccountState(
|
SelectAccountState(viewState = SelectAccountState.ViewState.Loading),
|
||||||
accountSelectionListItems = persistentListOf(),
|
|
||||||
),
|
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -79,7 +76,11 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SelectAccountState(accountSelectionListItems = persistentListOf(expectedItem)),
|
SelectAccountState(
|
||||||
|
viewState = SelectAccountState.ViewState.Content(
|
||||||
|
accountSelectionListItems = persistentListOf(expectedItem),
|
||||||
|
),
|
||||||
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -116,9 +117,7 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SelectAccountState(
|
SelectAccountState(viewState = SelectAccountState.ViewState.NoItems),
|
||||||
accountSelectionListItems = persistentListOf(),
|
|
||||||
),
|
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -163,7 +162,11 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
SelectAccountState(accountSelectionListItems = persistentListOf(expectedItem)),
|
SelectAccountState(
|
||||||
|
viewState = SelectAccountState.ViewState.Content(
|
||||||
|
accountSelectionListItems = persistentListOf(expectedItem),
|
||||||
|
),
|
||||||
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.bitwarden.ui.platform.components.content
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
@ -12,11 +13,14 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bitwarden.ui.platform.base.util.nullableTestTag
|
import com.bitwarden.ui.platform.base.util.nullableTestTag
|
||||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||||
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
|
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
|
||||||
import com.bitwarden.ui.platform.components.icon.model.IconData
|
import com.bitwarden.ui.platform.components.icon.model.IconData
|
||||||
|
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
|
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,6 +32,8 @@ fun BitwardenEmptyContent(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
illustrationData: IconData? = null,
|
illustrationData: IconData? = null,
|
||||||
labelTestTag: String? = null,
|
labelTestTag: String? = null,
|
||||||
|
title: String? = null,
|
||||||
|
titleTestTag: String? = null,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@ -41,6 +47,19 @@ fun BitwardenEmptyContent(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||||
}
|
}
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = BitwardenTheme.typography.titleMedium,
|
||||||
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin()
|
||||||
|
.nullableTestTag(tag = titleTestTag),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = BitwardenTheme.typography.bodyMedium,
|
style = BitwardenTheme.typography.bodyMedium,
|
||||||
@ -54,3 +73,20 @@ fun BitwardenEmptyContent(
|
|||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Bitwarden empty content")
|
||||||
|
@Composable
|
||||||
|
private fun BitwardenEmptyContent_preview() {
|
||||||
|
BitwardenScaffold {
|
||||||
|
BitwardenEmptyContent(
|
||||||
|
title = "Empty content",
|
||||||
|
titleTestTag = "TitleTestTag",
|
||||||
|
text = "There is no content to display",
|
||||||
|
labelTestTag = "EmptyContentLabel",
|
||||||
|
illustrationData = IconData.Local(BitwardenDrawable.ic_empty_vault),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package com.bitwarden.ui.platform.components.content
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@ -11,8 +12,11 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||||
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularProgressIndicator
|
import com.bitwarden.ui.platform.components.indicator.BitwardenCircularProgressIndicator
|
||||||
|
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,3 +50,16 @@ fun BitwardenLoadingContent(
|
|||||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, name = "Bitwarden loading content")
|
||||||
|
@Composable
|
||||||
|
private fun BitwardenLoadingContent_preview() {
|
||||||
|
BitwardenScaffold {
|
||||||
|
BitwardenLoadingContent(
|
||||||
|
text = "Loading...",
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1119,6 +1119,8 @@ Do you want to switch to this account?</string>
|
|||||||
<string name="not_now">Not now</string>
|
<string name="not_now">Not now</string>
|
||||||
<string name="import_from_bitwarden">Import from Bitwarden</string>
|
<string name="import_from_bitwarden">Import from Bitwarden</string>
|
||||||
<string name="select_account">Select account</string>
|
<string name="select_account">Select account</string>
|
||||||
|
<string name="no_accounts_available">No accounts available</string>
|
||||||
|
<string name="you_dont_have_any_accounts_you_can_import_from">You don’t have any accounts you can import from. Your organization’s security policy may restrict importing items from Bitwarden to another app.</string>
|
||||||
<string name="import_restricted_unable_to_import_credit_cards">Import restricted, unable to import cards from this account.</string>
|
<string name="import_restricted_unable_to_import_credit_cards">Import restricted, unable to import cards from this account.</string>
|
||||||
<string name="verify_your_master_password">Verify your master password</string>
|
<string name="verify_your_master_password">Verify your master password</string>
|
||||||
<string name="having_trouble_with_autofill">Having trouble with autofill?</string>
|
<string name="having_trouble_with_autofill">Having trouble with autofill?</string>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user