[PM-26355] Improve SelectAccountScreen state handling (#5965)

This commit is contained in:
Patrick Honkonen 2025-10-02 17:05:08 -04:00 committed by GitHub
parent 2eb829a25b
commit acc9113f9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 313 additions and 96 deletions

View File

@ -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(),
) )
} }
} }

View File

@ -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.

View File

@ -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,
),
), ),
), ),
) )

View File

@ -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,
) )
} }

View File

@ -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(),
)
}
}

View File

@ -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(),
)
}
}

View File

@ -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 dont have any accounts you can import from. Your organizations 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>