Compare commits

..

1 Commits

Author SHA1 Message Date
David Perez
509322c972 PM-20593: Always refresh the token before a sync request 2025-10-02 14:46:07 -05:00
15 changed files with 120 additions and 372 deletions

View File

@@ -27,9 +27,6 @@
],
"matchManagers": [
"gradle"
],
"excludePackageNames": [
"com.github.bumptech.glide:compose"
]
},
{

View File

@@ -1,24 +0,0 @@
name: Publish Password Manager and Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
permissions:
contents: write
id-token: write
actions: read
jobs:
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@agalles/test-1118
with:
release_name: "Password Manager"
workflow_name: "publish-github-release-bwpm.yml"
credentials_filename: "play_creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit

View File

@@ -3,7 +3,7 @@ name: Publish Password Manager and Authenticator GitHub Release as newest
on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Every hour on the hour on weekdays
- cron: '0 3 * * 1-5'
permissions:
contents: write
@@ -11,12 +11,24 @@ permissions:
actions: read
jobs:
publish-release-password-manager:
name: Publish Password Manager Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Password Manager"
workflow_name: "publish-github-release.yml"
credentials_filename: "play_creds.json"
project_type: android
check_release_command: >
bundle exec fastlane getLatestPlayStoreVersion package_name:com.x8bit.bitwarden track:production
secrets: inherit
publish-release-authenticator:
name: Publish Authenticator Release
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@agalles/test-1118
uses: bitwarden/gh-actions/.github/workflows/_publish-mobile-github-release.yml@main
with:
release_name: "Authenticator"
workflow_name: "publish-github-release-bwa.yml"
workflow_name: "publish-github-release.yml"
credentials_filename: "authenticator_play_store-creds.json"
project_type: android
check_release_command: >

View File

@@ -1,6 +1,5 @@
name: Publish to Google Play
run-name: >
${{ inputs.dry-run && ' (Dry Run)' || '' }}Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}
run-name: "Promoting ${{ inputs.product }} ${{ inputs.version-code }} from ${{ inputs.track-from }} to ${{ inputs.track-target }}"
on:
workflow_dispatch:
inputs:
@@ -47,10 +46,6 @@ on:
- production
- Fastlane Automation Target
required: true
dry-run:
description: "Dry-Run, Run the workflow without publishing to the store"
type: boolean
default: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_ACTION_RUN_URL: "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
@@ -59,7 +54,6 @@ permissions:
contents: read
packages: read
id-token: write
actions: write
jobs:
promote:
@@ -128,8 +122,6 @@ jobs:
echo "$FORMATTED_MESSAGE" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Promote Play Store version to production
if: ${{ inputs.dry-run == false }}
id: publish
env:
PLAY_KEYSTORE_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEYSTORE-PASSWORD }}
PLAY_KEY_PASSWORD: ${{ steps.get-kv-secrets.outputs.PLAY-BETA-KEY-PASSWORD }}
@@ -166,19 +158,3 @@ jobs:
releaseNotes:"$RELEASE_NOTES" \
track:"$TRACK_FROM" \
trackPromoteTo:"$TRACK_TARGET"
- name: Enable Publish Github Release Workflow
if: ${{ steps.publish.conclusion == 'success' || inputs.dry-run }}
env:
PRODUCT: ${{ inputs.product }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
if $DRY_RUN ; then
gh workflow view publish-github-release.yml
exit 0
fi
if [ "$PRODUCT" = "Password Manager" ]; then
gh workflow enable publish-github-release-bwpm.yml
elif [ "$PRODUCT" = "Authenticator" ]; then
gh workflow enable publish-github-release-bwa.yml
fi

View File

@@ -303,8 +303,6 @@ class AuthRepositoryImpl(
.syncOrgKeysFlow
.onEach { userId ->
if (userId == activeUserId) {
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
refreshAccessTokenSynchronously(userId = userId)
// We just sync now to get the latest data
vaultRepository.sync(forced = true)
} else {

View File

@@ -4,7 +4,6 @@ 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
@@ -19,7 +18,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.ImportCredentialsCancellationException
import androidx.credentials.providerevents.exception.ImportCredentialsUnknownErrorException
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
@@ -28,8 +27,7 @@ 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.content.BitwardenEmptyContent
import com.bitwarden.ui.platform.components.content.BitwardenLoadingContent
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
@@ -38,7 +36,6 @@ 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
/**
@@ -46,7 +43,6 @@ import kotlinx.collections.immutable.persistentListOf
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Suppress("LongMethod")
fun SelectAccountScreen(
onAccountSelected: (userId: String) -> Unit,
viewModel: SelectAccountViewModel = hiltViewModel(),
@@ -61,7 +57,9 @@ fun SelectAccountScreen(
credentialExchangeCompletionManager
.completeCredentialExport(
exportResult = ExportCredentialsResult.Failure(
error = ImportCredentialsCancellationException(
// TODO: [PM-26094] Use ImportCredentialsCancellationException once
// public.
error = ImportCredentialsUnknownErrorException(
errorMessage = "User cancelled import.",
),
),
@@ -84,40 +82,19 @@ fun SelectAccountScreen(
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
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(),
)
}
}
SelectAccountContent(
state = state,
onAccountClick = handlers.onAccountClick,
modifier = Modifier
.fillMaxSize()
.standardHorizontalMargin(),
)
}
}
@Composable
private fun SelectAccountContent(
accountSelectionListItems: ImmutableList<AccountSelectionListItem>,
state: SelectAccountState,
onAccountClick: (userId: String) -> Unit,
modifier: Modifier = Modifier,
) {
@@ -129,121 +106,62 @@ private fun SelectAccountContent(
text = stringResource(BitwardenString.select_account),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.titleMedium,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
modifier = Modifier.fillMaxWidth(),
)
}
item { Spacer(Modifier.height(16.dp)) }
itemsIndexed(
items = accountSelectionListItems,
items = state.accountSelectionListItems,
key = { _, item -> "AccountSummaryItem_${item.userId}" },
) { index, item ->
AccountSummaryListItem(
item = item,
cardStyle = accountSelectionListItems.toListItemCardStyle(index),
cardStyle = state.accountSelectionListItems.toListItemCardStyle(index),
clickable = true,
onClick = onAccountClick,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin()
.animateItem(),
)
}
item { Spacer(Modifier.height(16.dp)) }
item { Spacer(Modifier.navigationBarsPadding()) }
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(
showBackground = true,
name = "Select account content",
showSystemUi = true,
)
@Preview(showBackground = true)
@Composable
private fun SelectAccountContent_preview() {
ExportItemsScaffold(
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
navigationIconContentDescription = stringResource(BitwardenString.close),
onNavigationIconClick = { },
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
) {
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 {
SelectAccountContent(
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,
),
),
state = state,
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,6 +1,5 @@
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
@@ -12,13 +11,12 @@ 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
/**
@@ -30,7 +28,7 @@ class SelectAccountViewModel @Inject constructor(
policyManager: PolicyManager,
) : BaseViewModel<SelectAccountState, SelectAccountEvent, SelectAccountAction>(
initialState = SelectAccountState(
viewState = SelectAccountState.ViewState.Loading,
accountSelectionListItems = persistentListOf(),
),
) {
@@ -97,38 +95,30 @@ 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(
viewState = if (accountSelectionListItems.isEmpty()) {
SelectAccountState.ViewState.NoItems
} else {
SelectAccountState.ViewState.Content(
accountSelectionListItems = accountSelectionListItems,
)
},
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(),
)
}
}
@@ -136,40 +126,12 @@ class SelectAccountViewModel @Inject constructor(
/**
* 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 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()
}
}
val accountSelectionListItems: ImmutableList<AccountSelectionListItem>,
)
/**
* Represents the actions that can be performed on the select account screen.

View File

@@ -1,7 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.exportitems
import androidx.compose.ui.test.isDisplayed
import androidx.compose.ui.test.isNotDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@@ -125,42 +124,6 @@ class SelectAccountScreenTest : BitwardenComposeTest() {
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(
@@ -185,22 +148,20 @@ private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
)
private val DEFAULT_STATE = SelectAccountState(
viewState = SelectAccountState.ViewState.Content(
accountSelectionListItems = persistentListOf(
AccountSelectionListItem(
userId = ACTIVE_ACCOUNT_SUMMARY.userId,
email = ACTIVE_ACCOUNT_SUMMARY.email,
initials = "AA",
avatarColorHex = "#FFFF0000",
isItemRestricted = false,
),
AccountSelectionListItem(
userId = LOCKED_ACCOUNT_SUMMARY.userId,
email = LOCKED_ACCOUNT_SUMMARY.email,
initials = "LU",
avatarColorHex = "#FF00FF00",
isItemRestricted = false,
),
accountSelectionListItems = persistentListOf(
AccountSelectionListItem(
userId = ACTIVE_ACCOUNT_SUMMARY.userId,
email = ACTIVE_ACCOUNT_SUMMARY.email,
initials = "AA",
avatarColorHex = "#FFFF0000",
isItemRestricted = false,
),
AccountSelectionListItem(
userId = LOCKED_ACCOUNT_SUMMARY.userId,
email = LOCKED_ACCOUNT_SUMMARY.email,
initials = "LU",
avatarColorHex = "#FF00FF00",
isItemRestricted = false,
),
),
)

View File

@@ -50,8 +50,11 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
@Test
fun `initial state should be correct`() = runTest {
val viewModel = createViewModel()
assertEquals(
SelectAccountState(viewState = SelectAccountState.ViewState.Loading),
SelectAccountState(
accountSelectionListItems = persistentListOf(),
),
viewModel.stateFlow.value,
)
}
@@ -76,11 +79,7 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
)
assertEquals(
SelectAccountState(
viewState = SelectAccountState.ViewState.Content(
accountSelectionListItems = persistentListOf(expectedItem),
),
),
SelectAccountState(accountSelectionListItems = persistentListOf(expectedItem)),
viewModel.stateFlow.value,
)
}
@@ -117,7 +116,9 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
),
)
assertEquals(
SelectAccountState(viewState = SelectAccountState.ViewState.NoItems),
SelectAccountState(
accountSelectionListItems = persistentListOf(),
),
viewModel.stateFlow.value,
)
}
@@ -162,11 +163,7 @@ class SelectAccountViewModelTest : BaseViewModelTest() {
),
)
assertEquals(
SelectAccountState(
viewState = SelectAccountState.ViewState.Content(
accountSelectionListItems = persistentListOf(expectedItem),
),
),
SelectAccountState(accountSelectionListItems = persistentListOf(expectedItem)),
viewModel.stateFlow.value,
)
}

View File

@@ -54,7 +54,7 @@ mockk = "1.14.5"
okhttp = "5.1.0"
retrofitBom = "3.0.0"
robolectric = "4.16"
sonarqube = "6.3.1.5724"
sonarqube = "6.2.0.5505"
testng = "7.11.0"
timber = "5.0.1"
turbine = "1.2.1"

View File

@@ -3,6 +3,7 @@ package com.bitwarden.network.api
import com.bitwarden.network.model.NetworkResult
import com.bitwarden.network.model.SyncResponseJson
import retrofit2.http.GET
import retrofit2.http.Headers
/**
* This interface defines the API service for fetching vault data.
@@ -13,6 +14,7 @@ internal interface SyncApi {
*
* @return A [SyncResponseJson] containing the vault response model.
*/
@Headers("Access-Token-Sync: true")
@GET("sync")
suspend fun sync(): NetworkResult<SyncResponseJson>

View File

@@ -15,6 +15,7 @@ import java.time.Clock
import java.time.Instant
import java.time.temporal.ChronoUnit
private const val AUTH_TOKEN_SYNC_HEADER: String = "Access-Token-Sync"
private const val MISSING_TOKEN_MESSAGE: String = "Auth token is missing!"
private const val MISSING_PROVIDER_MESSAGE: String = "Refresh token provider is missing!"
private const val EXPIRATION_OFFSET_MINUTES: Long = 5L
@@ -79,7 +80,9 @@ internal class AuthTokenManager(
val expirationTime = Instant
.ofEpochSecond(tokenData.expiresAtSec)
.minus(EXPIRATION_OFFSET_MINUTES, ChronoUnit.MINUTES)
if (clock.instant().isAfter(expirationTime)) {
if (clock.instant().isAfter(expirationTime) ||
chain.request().header(AUTH_TOKEN_SYNC_HEADER).toBoolean()
) {
Timber.d("Attempting to refresh token due to expiration")
refreshTokenProvider
?.refreshAccessTokenSynchronously(userId = tokenData.userId)
@@ -92,6 +95,7 @@ internal class AuthTokenManager(
val request = chain
.request()
.newBuilder()
.removeHeader(AUTH_TOKEN_SYNC_HEADER)
.addHeader(
name = HEADER_KEY_AUTHORIZATION,
value = "${HEADER_VALUE_BEARER_PREFIX}$token",

View File

@@ -3,7 +3,6 @@ package com.bitwarden.ui.platform.components.content
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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
@@ -13,14 +12,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.base.util.nullableTestTag
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.icon.BitwardenIcon
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
/**
@@ -32,8 +28,6 @@ fun BitwardenEmptyContent(
modifier: Modifier = Modifier,
illustrationData: IconData? = null,
labelTestTag: String? = null,
title: String? = null,
titleTestTag: String? = null,
) {
Column(
modifier = modifier,
@@ -47,19 +41,6 @@ fun BitwardenEmptyContent(
)
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,
style = BitwardenTheme.typography.bodyMedium,
@@ -73,20 +54,3 @@ fun BitwardenEmptyContent(
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,7 +3,6 @@ package com.bitwarden.ui.platform.components.content
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.size
@@ -12,11 +11,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
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.scaffold.BitwardenScaffold
import com.bitwarden.ui.platform.theme.BitwardenTheme
/**
@@ -50,16 +46,3 @@ fun BitwardenLoadingContent(
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,8 +1119,6 @@ Do you want to switch to this account?</string>
<string name="not_now">Not now</string>
<string name="import_from_bitwarden">Import from Bitwarden</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="verify_your_master_password">Verify your master password</string>
<string name="having_trouble_with_autofill">Having trouble with autofill?</string>