mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 09:56:45 -06:00
[PM-26803] Show empty state when no items are available for export (#6023)
This commit is contained in:
parent
5b5176db40
commit
af737b3f07
@ -7,6 +7,7 @@ import androidx.navigation.NavGraphBuilder
|
|||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import com.bitwarden.annotation.OmitFromCoverage
|
import com.bitwarden.annotation.OmitFromCoverage
|
||||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.exportitems.selectaccount.popUpToSelectAccountScreen
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +35,7 @@ fun NavGraphBuilder.reviewExportDestination(
|
|||||||
composableWithPushTransitions<ReviewExportRoute> {
|
composableWithPushTransitions<ReviewExportRoute> {
|
||||||
ReviewExportScreen(
|
ReviewExportScreen(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
onNavigateToAccountSelection = { navController.popUpToSelectAccountScreen() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -32,7 +31,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
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
|
||||||
import com.bitwarden.cxf.model.ImportCredentialsRequestData
|
|
||||||
import com.bitwarden.cxf.ui.composition.LocalCredentialExchangeCompletionManager
|
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.cardStyle
|
import com.bitwarden.ui.platform.base.util.cardStyle
|
||||||
@ -40,6 +38,8 @@ 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.button.BitwardenFilledButton
|
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||||
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||||
|
import com.bitwarden.ui.platform.components.button.model.BitwardenButtonData
|
||||||
|
import com.bitwarden.ui.platform.components.content.BitwardenEmptyContent
|
||||||
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||||
@ -72,10 +72,12 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport.handlers.re
|
|||||||
* export process.
|
* export process.
|
||||||
* Defaults to the manager provided by [LocalCredentialExchangeCompletionManager].
|
* Defaults to the manager provided by [LocalCredentialExchangeCompletionManager].
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReviewExportScreen(
|
fun ReviewExportScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
|
onNavigateToAccountSelection: () -> Unit,
|
||||||
viewModel: ReviewExportViewModel = hiltViewModel(),
|
viewModel: ReviewExportViewModel = hiltViewModel(),
|
||||||
credentialExchangeCompletionManager: CredentialExchangeCompletionManager =
|
credentialExchangeCompletionManager: CredentialExchangeCompletionManager =
|
||||||
LocalCredentialExchangeCompletionManager.current,
|
LocalCredentialExchangeCompletionManager.current,
|
||||||
@ -86,6 +88,7 @@ fun ReviewExportScreen(
|
|||||||
EventsEffect(viewModel) {
|
EventsEffect(viewModel) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is ReviewExportEvent.NavigateBack -> onNavigateBack()
|
is ReviewExportEvent.NavigateBack -> onNavigateBack()
|
||||||
|
is ReviewExportEvent.NavigateToAccountSelection -> onNavigateToAccountSelection()
|
||||||
is ReviewExportEvent.CompleteExport -> {
|
is ReviewExportEvent.CompleteExport -> {
|
||||||
credentialExchangeCompletionManager.completeCredentialExport(it.result)
|
credentialExchangeCompletionManager.completeCredentialExport(it.result)
|
||||||
}
|
}
|
||||||
@ -107,14 +110,39 @@ fun ReviewExportScreen(
|
|||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
ReviewExportContent(
|
when (val viewState = state.viewState) {
|
||||||
state = state,
|
ReviewExportState.ViewState.NoItems -> {
|
||||||
onImportItemsClick = handler.onImportItemsClick,
|
BitwardenEmptyContent(
|
||||||
onCancelClick = handler.onCancelClick,
|
title = stringResource(BitwardenString.no_items_available_to_import),
|
||||||
modifier = Modifier
|
text = stringResource(
|
||||||
.fillMaxSize()
|
BitwardenString
|
||||||
.standardHorizontalMargin(),
|
.your_vault_may_be_empty_or_import_some_item_types_isnt_supported,
|
||||||
)
|
),
|
||||||
|
primaryButton = BitwardenButtonData(
|
||||||
|
label = BitwardenString.select_a_different_account.asText(),
|
||||||
|
testTag = "SelectADifferentAccountButton",
|
||||||
|
onClick = handler.onSelectAnotherAccountClick,
|
||||||
|
),
|
||||||
|
secondaryButton = BitwardenButtonData(
|
||||||
|
label = BitwardenString.cancel.asText(),
|
||||||
|
testTag = "NoItemsCancelButton",
|
||||||
|
onClick = handler.onCancelClick,
|
||||||
|
),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReviewExportState.ViewState.Content -> {
|
||||||
|
ReviewExportContent(
|
||||||
|
content = viewState,
|
||||||
|
onImportItemsClick = handler.onImportItemsClick,
|
||||||
|
onCancelClick = handler.onCancelClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +181,7 @@ private fun ReviewExportDialogs(
|
|||||||
* This composable lays out the illustrative image, titles, list of items to export,
|
* This composable lays out the illustrative image, titles, list of items to export,
|
||||||
* and action buttons.
|
* and action buttons.
|
||||||
*
|
*
|
||||||
* @param state The current [ReviewExportState] to render.
|
* @param content The current [ReviewExportState] to render.
|
||||||
* @param onImportItemsClick Callback invoked when the "Import Items" button is clicked.
|
* @param onImportItemsClick Callback invoked when the "Import Items" button is clicked.
|
||||||
* @param onCancelClick Callback invoked when the "Cancel" button is clicked.
|
* @param onCancelClick Callback invoked when the "Cancel" button is clicked.
|
||||||
* @param modifier The modifier to be applied to the content root.
|
* @param modifier The modifier to be applied to the content root.
|
||||||
@ -161,7 +189,7 @@ private fun ReviewExportDialogs(
|
|||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReviewExportContent(
|
private fun ReviewExportContent(
|
||||||
state: ReviewExportState,
|
content: ReviewExportState.ViewState.Content,
|
||||||
onImportItemsClick: () -> Unit,
|
onImportItemsClick: () -> Unit,
|
||||||
onCancelClick: () -> Unit,
|
onCancelClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@ -211,27 +239,27 @@ private fun ReviewExportContent(
|
|||||||
|
|
||||||
ItemCountRow(
|
ItemCountRow(
|
||||||
label = stringResource(BitwardenString.passwords).asText(),
|
label = stringResource(BitwardenString.passwords).asText(),
|
||||||
itemCount = state.viewState.itemTypeCounts.passwordCount,
|
itemCount = content.itemTypeCounts.passwordCount,
|
||||||
cardStyle = CardStyle.Top(),
|
cardStyle = CardStyle.Top(),
|
||||||
)
|
)
|
||||||
ItemCountRow(
|
ItemCountRow(
|
||||||
label = stringResource(BitwardenString.passkeys).asText(),
|
label = stringResource(BitwardenString.passkeys).asText(),
|
||||||
itemCount = state.viewState.itemTypeCounts.passkeyCount,
|
itemCount = content.itemTypeCounts.passkeyCount,
|
||||||
cardStyle = CardStyle.Middle(),
|
cardStyle = CardStyle.Middle(),
|
||||||
)
|
)
|
||||||
ItemCountRow(
|
ItemCountRow(
|
||||||
label = stringResource(BitwardenString.identities).asText(),
|
label = stringResource(BitwardenString.identities).asText(),
|
||||||
itemCount = state.viewState.itemTypeCounts.identityCount,
|
itemCount = content.itemTypeCounts.identityCount,
|
||||||
cardStyle = CardStyle.Middle(),
|
cardStyle = CardStyle.Middle(),
|
||||||
)
|
)
|
||||||
ItemCountRow(
|
ItemCountRow(
|
||||||
label = stringResource(BitwardenString.cards).asText(),
|
label = stringResource(BitwardenString.cards).asText(),
|
||||||
itemCount = state.viewState.itemTypeCounts.cardCount,
|
itemCount = content.itemTypeCounts.cardCount,
|
||||||
cardStyle = CardStyle.Middle(),
|
cardStyle = CardStyle.Middle(),
|
||||||
)
|
)
|
||||||
ItemCountRow(
|
ItemCountRow(
|
||||||
label = stringResource(BitwardenString.secure_notes).asText(),
|
label = stringResource(BitwardenString.secure_notes).asText(),
|
||||||
itemCount = state.viewState.itemTypeCounts.secureNoteCount,
|
itemCount = content.itemTypeCounts.secureNoteCount,
|
||||||
cardStyle = CardStyle.Bottom,
|
cardStyle = CardStyle.Bottom,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -298,24 +326,23 @@ private fun ItemCountRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Preview(showBackground = true, name = "Review Export Content")
|
@Preview(showBackground = true, name = "Review Export Content")
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReviewExportContent_preview() {
|
private fun ReviewExportContent_preview() {
|
||||||
BitwardenTheme {
|
ExportItemsScaffold(
|
||||||
|
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(BitwardenString.close),
|
||||||
|
onNavigationIconClick = { },
|
||||||
|
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||||
|
) {
|
||||||
ReviewExportContent(
|
ReviewExportContent(
|
||||||
state = ReviewExportState(
|
content = ReviewExportState.ViewState.Content(
|
||||||
importCredentialsRequestData = ImportCredentialsRequestData(
|
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||||
uri = Uri.EMPTY,
|
passwordCount = 14,
|
||||||
requestJson = "",
|
passkeyCount = 14,
|
||||||
),
|
identityCount = 3,
|
||||||
viewState = ReviewExportState.ViewState(
|
secureNoteCount = 5,
|
||||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
|
||||||
passwordCount = 14,
|
|
||||||
passkeyCount = 14,
|
|
||||||
identityCount = 3,
|
|
||||||
cardCount = 4,
|
|
||||||
secureNoteCount = 5,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onImportItemsClick = {},
|
onImportItemsClick = {},
|
||||||
@ -326,3 +353,35 @@ private fun ReviewExportContent_preview() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true, name = "Review Export Empty Content")
|
||||||
|
@Composable
|
||||||
|
private fun ReviewExportContent_NoItems_preview() {
|
||||||
|
ExportItemsScaffold(
|
||||||
|
navIcon = rememberVectorPainter(BitwardenDrawable.ic_close),
|
||||||
|
navigationIconContentDescription = stringResource(BitwardenString.close),
|
||||||
|
onNavigationIconClick = { },
|
||||||
|
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
|
||||||
|
) {
|
||||||
|
BitwardenEmptyContent(
|
||||||
|
title = stringResource(BitwardenString.no_items_available_to_import),
|
||||||
|
text = stringResource(
|
||||||
|
BitwardenString
|
||||||
|
.your_vault_may_be_empty_or_import_some_item_types_isnt_supported,
|
||||||
|
),
|
||||||
|
primaryButton = BitwardenButtonData(
|
||||||
|
label = BitwardenString.select_a_different_account.asText(),
|
||||||
|
testTag = "SelectADifferentAccountButton",
|
||||||
|
onClick = { },
|
||||||
|
),
|
||||||
|
secondaryButton = BitwardenButtonData(
|
||||||
|
label = BitwardenString.cancel.asText(),
|
||||||
|
testTag = "NoItemsCancelButton",
|
||||||
|
onClick = { },
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.credentials.providerevents.exception.ImportCredentialsCancellationException
|
||||||
import androidx.credentials.providerevents.exception.ImportCredentialsException
|
import androidx.credentials.providerevents.exception.ImportCredentialsException
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitwarden.core.data.repository.model.DataState
|
import com.bitwarden.core.data.repository.model.DataState
|
||||||
@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -53,9 +55,10 @@ class ReviewExportViewModel @Inject constructor(
|
|||||||
.specialCircumstance
|
.specialCircumstance
|
||||||
?.toImportCredentialsRequestDataOrNull(),
|
?.toImportCredentialsRequestDataOrNull(),
|
||||||
),
|
),
|
||||||
viewState = ReviewExportState.ViewState(
|
viewState = ReviewExportState.ViewState.Content(
|
||||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
||||||
),
|
),
|
||||||
|
dialog = null,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@ -73,6 +76,7 @@ class ReviewExportViewModel @Inject constructor(
|
|||||||
is ReviewExportAction.CancelClick -> handleCancelClicked()
|
is ReviewExportAction.CancelClick -> handleCancelClicked()
|
||||||
is ReviewExportAction.DismissDialog -> handleDismissDialog()
|
is ReviewExportAction.DismissDialog -> handleDismissDialog()
|
||||||
is ReviewExportAction.NavigateBackClick -> handleBackClick()
|
is ReviewExportAction.NavigateBackClick -> handleBackClick()
|
||||||
|
is ReviewExportAction.SelectAnotherAccountClick -> handleSelectAnotherAccountClick()
|
||||||
is ReviewExportAction.Internal -> handleInternalAction(action)
|
is ReviewExportAction.Internal -> handleInternalAction(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,7 +115,13 @@ class ReviewExportViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun handleCancelClicked() {
|
private fun handleCancelClicked() {
|
||||||
sendEvent(ReviewExportEvent.NavigateBack)
|
sendEvent(
|
||||||
|
ReviewExportEvent.CompleteExport(
|
||||||
|
ExportCredentialsResult.Failure(
|
||||||
|
ImportCredentialsCancellationException(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleDismissDialog() {
|
private fun handleDismissDialog() {
|
||||||
@ -122,6 +132,10 @@ class ReviewExportViewModel @Inject constructor(
|
|||||||
sendEvent(ReviewExportEvent.NavigateBack)
|
sendEvent(ReviewExportEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSelectAnotherAccountClick() {
|
||||||
|
sendEvent(ReviewExportEvent.NavigateToAccountSelection)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleInternalAction(action: ReviewExportAction.Internal) {
|
private fun handleInternalAction(action: ReviewExportAction.Internal) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is ReviewExportAction.Internal.VaultDataReceive -> {
|
is ReviewExportAction.Internal.VaultDataReceive -> {
|
||||||
@ -168,7 +182,7 @@ class ReviewExportViewModel @Inject constructor(
|
|||||||
private fun handleVaultDataError(data: DataState.Error<DecryptCipherListResult>) {
|
private fun handleVaultDataError(data: DataState.Error<DecryptCipherListResult>) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = it.viewState.copy(
|
viewState = ReviewExportState.ViewState.Content(
|
||||||
itemTypeCounts = data.data.toItemTypeCounts(),
|
itemTypeCounts = data.data.toItemTypeCounts(),
|
||||||
),
|
),
|
||||||
dialog = ReviewExportState.DialogState.General(
|
dialog = ReviewExportState.DialogState.General(
|
||||||
@ -253,10 +267,17 @@ class ReviewExportViewModel @Inject constructor(
|
|||||||
clearDialog: Boolean,
|
clearDialog: Boolean,
|
||||||
) {
|
) {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
|
val itemTypeCounts = data.data.toItemTypeCounts()
|
||||||
|
val viewState = if (itemTypeCounts.hasItemsToExport) {
|
||||||
|
ReviewExportState.ViewState.Content(
|
||||||
|
itemTypeCounts = itemTypeCounts,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ReviewExportState.ViewState.NoItems
|
||||||
|
}
|
||||||
|
|
||||||
it.copy(
|
it.copy(
|
||||||
viewState = it.viewState.copy(
|
viewState = viewState,
|
||||||
itemTypeCounts = data.data.toItemTypeCounts(),
|
|
||||||
),
|
|
||||||
dialog = it.dialog.takeUnless { clearDialog },
|
dialog = it.dialog.takeUnless { clearDialog },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -281,9 +302,20 @@ data class ReviewExportState(
|
|||||||
* Represents the view state with item type counts.
|
* Represents the view state with item type counts.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ViewState(
|
sealed class ViewState : Parcelable {
|
||||||
val itemTypeCounts: ItemTypeCounts,
|
|
||||||
) : Parcelable
|
/**
|
||||||
|
* Represents the content state with item type counts.
|
||||||
|
*/
|
||||||
|
data class Content(
|
||||||
|
val itemTypeCounts: ItemTypeCounts,
|
||||||
|
) : ViewState()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state when there are no items to be exported.
|
||||||
|
*/
|
||||||
|
data object NoItems : ViewState()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the counts of different item types to be exported.
|
* Represents the counts of different item types to be exported.
|
||||||
@ -295,7 +327,17 @@ data class ReviewExportState(
|
|||||||
val identityCount: Int = 0,
|
val identityCount: Int = 0,
|
||||||
val cardCount: Int = 0,
|
val cardCount: Int = 0,
|
||||||
val secureNoteCount: Int = 0,
|
val secureNoteCount: Int = 0,
|
||||||
) : Parcelable
|
) : Parcelable {
|
||||||
|
/**
|
||||||
|
* Whether there are any items to be exported.
|
||||||
|
*/
|
||||||
|
@IgnoredOnParcel
|
||||||
|
val hasItemsToExport: Boolean = passwordCount > 0 ||
|
||||||
|
passkeyCount > 0 ||
|
||||||
|
identityCount > 0 ||
|
||||||
|
cardCount > 0 ||
|
||||||
|
secureNoteCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the possible dialog states for the Review Import screen.
|
* Represents the possible dialog states for the Review Import screen.
|
||||||
@ -350,6 +392,11 @@ sealed class ReviewExportAction {
|
|||||||
*/
|
*/
|
||||||
data object NavigateBackClick : ReviewExportAction()
|
data object NavigateBackClick : ReviewExportAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action triggered when the Select another account button is clicked by the user.
|
||||||
|
*/
|
||||||
|
data object SelectAnotherAccountClick : ReviewExportAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal actions that the [ReviewExportViewModel] itself may send.
|
* Internal actions that the [ReviewExportViewModel] itself may send.
|
||||||
*/
|
*/
|
||||||
@ -382,6 +429,11 @@ sealed class ReviewExportEvent {
|
|||||||
*/
|
*/
|
||||||
data object NavigateBack : ReviewExportEvent()
|
data object NavigateBack : ReviewExportEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event to navigate to account selection.
|
||||||
|
*/
|
||||||
|
data object NavigateToAccountSelection : ReviewExportEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event indicating that the import attempt has completed.
|
* Event indicating that the import attempt has completed.
|
||||||
* The consuming screen or navigation controller should handle this event to proceed
|
* The consuming screen or navigation controller should handle this event to proceed
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport.ReviewExpor
|
|||||||
*/
|
*/
|
||||||
data class ReviewExportHandlers(
|
data class ReviewExportHandlers(
|
||||||
val onImportItemsClick: () -> Unit,
|
val onImportItemsClick: () -> Unit,
|
||||||
|
val onSelectAnotherAccountClick: () -> Unit,
|
||||||
val onCancelClick: () -> Unit,
|
val onCancelClick: () -> Unit,
|
||||||
val onDismissDialog: () -> Unit,
|
val onDismissDialog: () -> Unit,
|
||||||
val onNavigateBackClick: () -> Unit,
|
val onNavigateBackClick: () -> Unit,
|
||||||
@ -36,6 +37,9 @@ data class ReviewExportHandlers(
|
|||||||
onImportItemsClick = {
|
onImportItemsClick = {
|
||||||
viewModel.trySendAction(ReviewExportAction.ImportItemsClick)
|
viewModel.trySendAction(ReviewExportAction.ImportItemsClick)
|
||||||
},
|
},
|
||||||
|
onSelectAnotherAccountClick = {
|
||||||
|
viewModel.trySendAction(ReviewExportAction.SelectAnotherAccountClick)
|
||||||
|
},
|
||||||
onCancelClick = {
|
onCancelClick = {
|
||||||
viewModel.trySendAction(ReviewExportAction.CancelClick)
|
viewModel.trySendAction(ReviewExportAction.CancelClick)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -40,3 +40,13 @@ fun NavController.navigateToSelectAccountScreen(
|
|||||||
navOptions = navOptions,
|
navOptions = navOptions,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop up to the [SelectAccountScreen].
|
||||||
|
*/
|
||||||
|
fun NavController.popUpToSelectAccountScreen() {
|
||||||
|
popBackStack(
|
||||||
|
route = SelectAccountRoute,
|
||||||
|
inclusive = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
package com.x8bit.bitwarden.ui.vault.feature.exportitems.reviewexport
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.assertIsNotDisplayed
|
||||||
import androidx.compose.ui.test.filterToOne
|
import androidx.compose.ui.test.filterToOne
|
||||||
import androidx.compose.ui.test.hasAnyAncestor
|
import androidx.compose.ui.test.hasAnyAncestor
|
||||||
import androidx.compose.ui.test.isDialog
|
import androidx.compose.ui.test.isDialog
|
||||||
import androidx.compose.ui.test.isDisplayed
|
import androidx.compose.ui.test.isDisplayed
|
||||||
import androidx.compose.ui.test.onAllNodesWithText
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||||
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
|
import com.bitwarden.cxf.manager.CredentialExchangeCompletionManager
|
||||||
@ -28,6 +31,7 @@ import org.junit.Test
|
|||||||
class ReviewExportScreenTest : BitwardenComposeTest() {
|
class ReviewExportScreenTest : BitwardenComposeTest() {
|
||||||
|
|
||||||
private var onNavigateBackCalled = false
|
private var onNavigateBackCalled = false
|
||||||
|
private var onSelectAnotherAccountCalled = false
|
||||||
private val credentialExchangeCompletionManager = mockk<CredentialExchangeCompletionManager> {
|
private val credentialExchangeCompletionManager = mockk<CredentialExchangeCompletionManager> {
|
||||||
every { completeCredentialExport(any()) } just runs
|
every { completeCredentialExport(any()) } just runs
|
||||||
}
|
}
|
||||||
@ -46,6 +50,7 @@ class ReviewExportScreenTest : BitwardenComposeTest() {
|
|||||||
) {
|
) {
|
||||||
ReviewExportScreen(
|
ReviewExportScreen(
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
onNavigateToAccountSelection = { onSelectAnotherAccountCalled = true },
|
||||||
viewModel = mockViewModel,
|
viewModel = mockViewModel,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -134,11 +139,52 @@ class ReviewExportScreenTest : BitwardenComposeTest() {
|
|||||||
mockViewModel.trySendAction(ReviewExportAction.NavigateBackClick)
|
mockViewModel.trySendAction(ReviewExportAction.NavigateBackClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EmptyContent should be displayed when no items to import`() {
|
||||||
|
// Verify initial state is ReviewExportContent
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("No items available to import")
|
||||||
|
.assertIsNotDisplayed()
|
||||||
|
|
||||||
|
mockStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ReviewExportState.ViewState.NoItems,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("No items available to import")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SelectAnotherAccount click should send SelectAnotherAccountClick action`() {
|
||||||
|
mockStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ReviewExportState.ViewState.NoItems,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Select a different account")
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
mockViewModel.trySendAction(ReviewExportAction.SelectAnotherAccountClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_STATE = ReviewExportState(
|
private val DEFAULT_STATE = ReviewExportState(
|
||||||
viewState = ReviewExportState.ViewState(
|
viewState = ReviewExportState.ViewState.Content(
|
||||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||||
|
passwordCount = 1,
|
||||||
|
passkeyCount = 1,
|
||||||
|
identityCount = 1,
|
||||||
|
cardCount = 1,
|
||||||
|
secureNoteCount = 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
importCredentialsRequestData = ImportCredentialsRequestData(
|
importCredentialsRequestData = ImportCredentialsRequestData(
|
||||||
uri = Uri.EMPTY,
|
uri = Uri.EMPTY,
|
||||||
|
|||||||
@ -46,12 +46,7 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
every { userStateFlow } returns mutableUserStateFlow
|
every { userStateFlow } returns mutableUserStateFlow
|
||||||
}
|
}
|
||||||
private val decryptCipherListResultFlow = MutableStateFlow<DataState<DecryptCipherListResult>>(
|
private val decryptCipherListResultFlow = MutableStateFlow<DataState<DecryptCipherListResult>>(
|
||||||
DataState.Loaded(
|
DataState.Loaded(data = createMockDecryptCipherListResult(number = 1)),
|
||||||
data = DecryptCipherListResult(
|
|
||||||
successes = emptyList(),
|
|
||||||
failures = emptyList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
private val vaultRepository = mockk<VaultRepository> {
|
private val vaultRepository = mockk<VaultRepository> {
|
||||||
every { decryptCipherListResultStateFlow } returns decryptCipherListResultFlow
|
every { decryptCipherListResultStateFlow } returns decryptCipherListResultFlow
|
||||||
@ -68,13 +63,42 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class Initialization {
|
inner class State {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state is correct when SavedStateHandle is empty`() =
|
fun `State should be NoItems when no items to export`() = runTest {
|
||||||
runTest {
|
val initialState = ReviewExportState(
|
||||||
val viewModel = createViewModel()
|
viewState = ReviewExportState.ViewState.NoItems,
|
||||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
dialog = null,
|
||||||
|
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
||||||
|
)
|
||||||
|
decryptCipherListResultFlow.value = DataState.Loaded(
|
||||||
|
data = DecryptCipherListResult(
|
||||||
|
successes = emptyList(),
|
||||||
|
failures = emptyList(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(initialState, awaitItem())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `State should be Content when items to export`() = runTest {
|
||||||
|
val expectedState = ReviewExportState(
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
|
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||||
|
passwordCount = 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dialog = null,
|
||||||
|
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
||||||
|
)
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(expectedState, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@ -83,12 +107,13 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `ImportItemsClick shows loading, and calls exportVaultDataToCxf with all active items if there are no item restrictions`() =
|
fun `ImportItemsClick shows loading, and calls exportVaultDataToCxf with all active items if there are no item restrictions`() =
|
||||||
runTest {
|
runTest {
|
||||||
val mockActiveCipherListView = createMockCipherListView(
|
val mockActiveCardCipherListView = createMockCipherListView(
|
||||||
number = 1,
|
number = 1,
|
||||||
type = CipherListViewType.Card(
|
type = CipherListViewType.Card(
|
||||||
createMockCardListView(number = 1),
|
createMockCardListView(number = 1),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
val mockActiveLoginCipherListView = createMockCipherListView(number = 1)
|
||||||
val mockDeletedCipherListView = createMockCipherListView(
|
val mockDeletedCipherListView = createMockCipherListView(
|
||||||
number = 1,
|
number = 1,
|
||||||
isDeleted = true,
|
isDeleted = true,
|
||||||
@ -98,14 +123,20 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
createMockDecryptCipherListResult(
|
createMockDecryptCipherListResult(
|
||||||
number = 1,
|
number = 1,
|
||||||
successes = listOf(
|
successes = listOf(
|
||||||
mockActiveCipherListView,
|
mockActiveLoginCipherListView,
|
||||||
|
mockActiveCardCipherListView,
|
||||||
mockDeletedCipherListView,
|
mockDeletedCipherListView,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
coEvery {
|
coEvery {
|
||||||
vaultRepository.exportVaultDataToCxf(listOf(mockActiveCipherListView))
|
vaultRepository.exportVaultDataToCxf(
|
||||||
|
listOf(
|
||||||
|
mockActiveLoginCipherListView,
|
||||||
|
mockActiveCardCipherListView,
|
||||||
|
),
|
||||||
|
)
|
||||||
} just awaits
|
} just awaits
|
||||||
|
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
@ -114,8 +145,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
// Check for loading dialog
|
// Check for loading dialog
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(
|
DEFAULT_STATE.copy(
|
||||||
viewState = DEFAULT_STATE.viewState.copy(
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||||
cardCount = 1,
|
cardCount = 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -127,7 +158,12 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
vaultRepository.exportVaultDataToCxf(listOf(mockActiveCipherListView))
|
vaultRepository.exportVaultDataToCxf(
|
||||||
|
listOf(
|
||||||
|
mockActiveLoginCipherListView,
|
||||||
|
mockActiveCardCipherListView,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,24 +243,41 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CancelClicked sends NavigateBack event`() = runTest {
|
fun `NavigateToAccountSelection sends SelectAnotherAccount event`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ReviewExportAction.SelectAnotherAccountClick)
|
||||||
|
assertEquals(
|
||||||
|
ReviewExportEvent.NavigateToAccountSelection,
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `CancelClicked sends CompleteExport event`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ReviewExportAction.CancelClick)
|
viewModel.trySendAction(ReviewExportAction.CancelClick)
|
||||||
assertEquals(ReviewExportEvent.NavigateBack, awaitItem())
|
assertTrue(awaitItem() is ReviewExportEvent.CompleteExport)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `DismissDialog clears dialog from state`() = runTest {
|
fun `DismissDialog clears dialog from state`() = runTest {
|
||||||
decryptCipherListResultFlow.value = DataState.Loading
|
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
val exception = IllegalStateException()
|
||||||
|
decryptCipherListResultFlow.value = DataState.Error(
|
||||||
|
error = exception,
|
||||||
|
data = createMockDecryptCipherListResult(number = 1),
|
||||||
|
)
|
||||||
// Check for loading dialog
|
// Check for loading dialog
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(
|
DEFAULT_STATE.copy(
|
||||||
dialog = ReviewExportState.DialogState.Loading(
|
dialog = ReviewExportState.DialogState.General(
|
||||||
BitwardenString.loading.asText(),
|
title = BitwardenString.an_error_has_occurred.asText(),
|
||||||
|
message = BitwardenString.generic_error_message.asText(),
|
||||||
|
error = exception,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
@ -254,8 +307,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
viewState = DEFAULT_STATE.viewState.copy(
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||||
passwordCount = 1,
|
passwordCount = 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -283,8 +336,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
viewState = DEFAULT_STATE.viewState.copy(
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||||
passwordCount = 1,
|
passwordCount = 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -312,8 +365,8 @@ class ReviewExportViewModelTest : BaseViewModelTest() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
val expectedState = DEFAULT_STATE.copy(
|
val expectedState = DEFAULT_STATE.copy(
|
||||||
viewState = DEFAULT_STATE.viewState.copy(
|
viewState = DEFAULT_CONTENT_VIEW_STATE.copy(
|
||||||
itemTypeCounts = DEFAULT_STATE.viewState.itemTypeCounts.copy(
|
itemTypeCounts = DEFAULT_CONTENT_VIEW_STATE.itemTypeCounts.copy(
|
||||||
passwordCount = 1,
|
passwordCount = 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -365,11 +418,14 @@ private val DEFAULT_REQUEST_DATA = ImportCredentialsRequestData(
|
|||||||
uri = MOCK_URI,
|
uri = MOCK_URI,
|
||||||
requestJson = "mockRequestJson",
|
requestJson = "mockRequestJson",
|
||||||
)
|
)
|
||||||
|
private val DEFAULT_CONTENT_VIEW_STATE = ReviewExportState.ViewState.Content(
|
||||||
|
itemTypeCounts = ReviewExportState.ItemTypeCounts(
|
||||||
|
passwordCount = 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
private val DEFAULT_STATE: ReviewExportState = ReviewExportState(
|
private val DEFAULT_STATE: ReviewExportState = ReviewExportState(
|
||||||
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
importCredentialsRequestData = DEFAULT_REQUEST_DATA,
|
||||||
viewState = ReviewExportState.ViewState(
|
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||||
itemTypeCounts = ReviewExportState.ItemTypeCounts(),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
private const val DEFAULT_USER_ID: String = "activeUserId"
|
private const val DEFAULT_USER_ID: String = "activeUserId"
|
||||||
private val DEFAULT_USER_STATE = UserState(
|
private val DEFAULT_USER_STATE = UserState(
|
||||||
|
|||||||
@ -1132,4 +1132,7 @@ Do you want to switch to this account?</string>
|
|||||||
<string name="kdf_update_failed_active_account_not_found">Kdf update failed, active account not found. Please try again or contact us.</string>
|
<string name="kdf_update_failed_active_account_not_found">Kdf update failed, active account not found. Please try again or contact us.</string>
|
||||||
<string name="an_error_occurred_while_trying_to_update_your_kdf_settings">An error occurred while trying to update your kdf settings. Please try again or contact us.</string>
|
<string name="an_error_occurred_while_trying_to_update_your_kdf_settings">An error occurred while trying to update your kdf settings. Please try again or contact us.</string>
|
||||||
<string name="the_import_request_could_not_be_processed">The import request could not be processed.</string>
|
<string name="the_import_request_could_not_be_processed">The import request could not be processed.</string>
|
||||||
|
<string name="your_vault_may_be_empty_or_import_some_item_types_isnt_supported">Your vault may be empty, or importing some item types isn’t allowed for your account.</string>
|
||||||
|
<string name="no_items_available_to_import">No items available to import</string>
|
||||||
|
<string name="select_a_different_account">Select a different account</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user