mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 20:07:59 -06:00
[PM-25825] Add ImportItems navigation (#5915)
Co-authored-by: David Perez <david@livefront.com>
This commit is contained in:
parent
bc1dd730ec
commit
8a2bcfade8
@ -32,6 +32,7 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.other.navigateToOther
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.other.otherDestination
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.navigateToVaultSettings
|
||||
import com.x8bit.bitwarden.ui.platform.feature.settings.vault.vaultSettingsDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importitems.importItemsDestination
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@ -114,6 +115,7 @@ fun NavGraphBuilder.settingsGraph(
|
||||
onNavigateToFlightRecorder: () -> Unit,
|
||||
onNavigateToRecordedLogs: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportItems: () -> Unit,
|
||||
onNavigateToAboutPrivilegedApps: () -> Unit,
|
||||
) {
|
||||
navigation<SettingsGraphRoute>(
|
||||
@ -162,6 +164,11 @@ fun NavGraphBuilder.settingsGraph(
|
||||
onNavigateToExportVault = onNavigateToExportVault,
|
||||
onNavigateToFolders = onNavigateToFolders,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToImportItems = onNavigateToImportItems,
|
||||
)
|
||||
importItemsDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
)
|
||||
blockAutoFillDestination(onNavigateBack = { navController.popBackStack() })
|
||||
privilegedAppsListDestination(onNavigateBack = { navController.popBackStack() })
|
||||
|
||||
@ -20,6 +20,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
|
||||
onNavigateToExportVault: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportItems: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions<VaultSettingsRoute> {
|
||||
VaultSettingsScreen(
|
||||
@ -27,6 +28,7 @@ fun NavGraphBuilder.vaultSettingsDestination(
|
||||
onNavigateToExportVault = onNavigateToExportVault,
|
||||
onNavigateToFolders = onNavigateToFolders,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToImportItems = onNavigateToImportItems,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ fun VaultSettingsScreen(
|
||||
onNavigateToExportVault: () -> Unit,
|
||||
onNavigateToFolders: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
onNavigateToImportItems: () -> Unit,
|
||||
viewModel: VaultSettingsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
@ -60,6 +61,7 @@ fun VaultSettingsScreen(
|
||||
VaultSettingsEvent.NavigateToExportVault -> onNavigateToExportVault()
|
||||
VaultSettingsEvent.NavigateToFolders -> onNavigateToFolders()
|
||||
is VaultSettingsEvent.NavigateToImportVault -> onNavigateToImportLogins()
|
||||
is VaultSettingsEvent.NavigateToImportItems -> onNavigateToImportItems()
|
||||
is VaultSettingsEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.ui.platform.base.BackgroundEvent
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
@ -22,6 +24,7 @@ import javax.inject.Inject
|
||||
class VaultSettingsViewModel @Inject constructor(
|
||||
snackbarRelayManager: SnackbarRelayManager,
|
||||
private val firstTimeActionManager: FirstTimeActionManager,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
) : BaseViewModel<VaultSettingsState, VaultSettingsEvent, VaultSettingsAction>(
|
||||
initialState = run {
|
||||
val firstTimeState = firstTimeActionManager.currentOrDefaultUserFirstTimeState
|
||||
@ -106,7 +109,11 @@ class VaultSettingsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
private fun handleImportItemsClicked() {
|
||||
sendEvent(VaultSettingsEvent.NavigateToImportVault)
|
||||
if (featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)) {
|
||||
sendEvent(VaultSettingsEvent.NavigateToImportItems)
|
||||
} else {
|
||||
sendEvent(VaultSettingsEvent.NavigateToImportVault)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +138,11 @@ sealed class VaultSettingsEvent {
|
||||
*/
|
||||
data object NavigateToImportVault : VaultSettingsEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the import vault URL.
|
||||
*/
|
||||
data object NavigateToImportItems : VaultSettingsEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the Export Vault screen.
|
||||
*/
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.ui.platform.feature.vaultunlockednavbar
|
||||
|
||||
import androidx.navigation.NavController
|
||||
|
||||
@ -40,6 +40,7 @@ import com.x8bit.bitwarden.ui.tools.feature.send.navigateToSendGraph
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.sendGraph
|
||||
import com.x8bit.bitwarden.ui.tools.feature.send.viewsend.ViewSendRoute
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.VaultAddEditArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importitems.navigateToImportItemsScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.VaultItemArgs
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultGraphRoute
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.navigateToVaultGraph
|
||||
@ -265,6 +266,7 @@ private fun VaultUnlockedNavBarScaffold(
|
||||
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
|
||||
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToImportItems = { navController.navigateToImportItemsScreen() },
|
||||
onNavigateToFlightRecorder = onNavigateToFlightRecorder,
|
||||
onNavigateToRecordedLogs = onNavigateToRecordedLogs,
|
||||
onNavigateToAboutPrivilegedApps = onNavigateToAboutPrivilegedApps,
|
||||
|
||||
@ -21,5 +21,4 @@ enum class SnackbarRelay {
|
||||
LOGINS_IMPORTED,
|
||||
SEND_DELETED,
|
||||
SEND_UPDATED,
|
||||
VAULT_SYNC_FAILED,
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.x8bit.bitwarden.ui.vault.feature.importitems
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.bitwarden.annotation.OmitFromCoverage
|
||||
import com.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The type-safe route for the import items screen.
|
||||
*/
|
||||
@Serializable
|
||||
data object ImportItemsRoute
|
||||
|
||||
/**
|
||||
* Helper function to navigate to the import items screen.
|
||||
*/
|
||||
fun NavController.navigateToImportItemsScreen(
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate(route = ImportItemsRoute, navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the import items screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.importItemsDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions<ImportItemsRoute> {
|
||||
ImportItemsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToImportFromComputer = onNavigateToImportLogins,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -30,6 +30,9 @@ import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
import com.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.bitwarden.ui.platform.components.row.BitwardenTextRow
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarHost
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarHostState
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
@ -46,7 +49,6 @@ import kotlinx.coroutines.launch
|
||||
@Composable
|
||||
fun ImportItemsScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVault: () -> Unit,
|
||||
onNavigateToImportFromComputer: () -> Unit,
|
||||
viewModel: ImportItemsViewModel = hiltViewModel(),
|
||||
credentialExchangeImporter: CredentialExchangeImporter =
|
||||
@ -55,11 +57,11 @@ fun ImportItemsScreen(
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val handler = rememberImportItemsHandler(viewModel = viewModel)
|
||||
val snackbarHostState = rememberBitwardenSnackbarHostState()
|
||||
|
||||
EventsEffect(viewModel) { event ->
|
||||
when (event) {
|
||||
ImportItemsEvent.NavigateBack -> onNavigateBack()
|
||||
ImportItemsEvent.NavigateToVault -> onNavigateToVault()
|
||||
ImportItemsEvent.NavigateToImportFromComputer -> onNavigateToImportFromComputer()
|
||||
is ImportItemsEvent.ShowRegisteredImportSources -> {
|
||||
coroutineScope.launch {
|
||||
@ -73,6 +75,17 @@ fun ImportItemsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ImportItemsEvent.ShowBasicSnackbar -> {
|
||||
snackbarHostState.showSnackbar(event.data)
|
||||
}
|
||||
|
||||
is ImportItemsEvent.ShowSyncFailedSnackbar -> {
|
||||
snackbarHostState.showSnackbar(
|
||||
snackbarData = event.data,
|
||||
onActionPerformed = handler.onSyncFailedTryAgainClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,6 +98,7 @@ fun ImportItemsScreen(
|
||||
onNavigateBack = handler.onNavigateBack,
|
||||
onImportFromComputerClick = handler.onImportFromComputerClick,
|
||||
onImportFromAnotherAppClick = handler.onImportFromAnotherAppClick,
|
||||
snackbarHostState = snackbarHostState,
|
||||
)
|
||||
}
|
||||
|
||||
@ -95,6 +109,7 @@ private fun ImportItemsScaffold(
|
||||
onImportFromComputerClick: () -> Unit,
|
||||
onImportFromAnotherAppClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(),
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
@ -112,6 +127,11 @@ private fun ImportItemsScaffold(
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
snackbarHost = {
|
||||
BitwardenSnackbarHost(
|
||||
bitwardenHostState = snackbarHostState,
|
||||
)
|
||||
},
|
||||
) {
|
||||
ImportItemsContent(
|
||||
onImportFromComputerClick = onImportFromComputerClick,
|
||||
|
||||
@ -13,10 +13,9 @@ import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@ -33,7 +32,6 @@ private const val KEY_STATE = "state"
|
||||
class ImportItemsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val snackbarRelayManager: SnackbarRelayManager,
|
||||
) : BaseViewModel<ImportItemsState, ImportItemsEvent, ImportItemsAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: ImportItemsState(),
|
||||
) {
|
||||
@ -52,14 +50,6 @@ class ImportItemsViewModel @Inject constructor(
|
||||
handleImportCredentialSelectionReceive(action)
|
||||
}
|
||||
|
||||
ImportItemsAction.ReturnToVaultClick -> {
|
||||
handleReturnToVaultClick()
|
||||
}
|
||||
|
||||
is ImportItemsAction.Internal.ImportCredentialsResultReceive -> {
|
||||
handleImportCredentialsResultReceive(action)
|
||||
}
|
||||
|
||||
ImportItemsAction.ImportFromComputerClick -> {
|
||||
handleImportFromComputerClick()
|
||||
}
|
||||
@ -67,11 +57,59 @@ class ImportItemsViewModel @Inject constructor(
|
||||
ImportItemsAction.DismissDialog -> {
|
||||
handleDismissDialog()
|
||||
}
|
||||
|
||||
ImportItemsAction.SyncFailedTryAgainClick -> {
|
||||
handleSyncFailedTryAgainClick()
|
||||
}
|
||||
|
||||
is ImportItemsAction.Internal -> {
|
||||
handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReturnToVaultClick() {
|
||||
sendEvent(ImportItemsEvent.NavigateToVault)
|
||||
private fun handleInternalAction(action: ImportItemsAction.Internal) {
|
||||
when (action) {
|
||||
is ImportItemsAction.Internal.ImportCredentialsResultReceive -> {
|
||||
handleImportCredentialsResultReceive(action)
|
||||
}
|
||||
|
||||
is ImportItemsAction.Internal.RetrySyncResultReceive -> {
|
||||
handleRetrySyncResultReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRetrySyncResultReceive(
|
||||
action: ImportItemsAction.Internal.RetrySyncResultReceive,
|
||||
) {
|
||||
clearDialogs()
|
||||
when (action.result) {
|
||||
is SyncVaultDataResult.Success -> {
|
||||
sendEvent(
|
||||
ImportItemsEvent.ShowBasicSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
message = BitwardenString.syncing_complete.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is SyncVaultDataResult.Error -> {
|
||||
showSyncFailedSnackbar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSyncFailedTryAgainClick() {
|
||||
showLoadingDialog(message = BitwardenString.syncing.asText())
|
||||
viewModelScope.launch {
|
||||
sendAction(
|
||||
ImportItemsAction.Internal.RetrySyncResultReceive(
|
||||
result = vaultRepository.syncForResult(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBackClick() {
|
||||
@ -118,7 +156,7 @@ class ImportItemsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
is ImportCredentialsSelectionResult.Success -> {
|
||||
updateImportProgress(BitwardenString.saving_items.asText())
|
||||
showLoadingDialog(BitwardenString.saving_items.asText())
|
||||
viewModelScope.launch {
|
||||
sendAction(
|
||||
ImportItemsAction.Internal.ImportCredentialsResultReceive(
|
||||
@ -150,19 +188,19 @@ class ImportItemsViewModel @Inject constructor(
|
||||
|
||||
is ImportCredentialsResult.Success -> {
|
||||
clearDialogs()
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.import_successful.asText(),
|
||||
message = BitwardenPlurals
|
||||
.x_items_have_been_imported_to_your_vault
|
||||
.asPluralsText(
|
||||
quantity = action.result.itemCount,
|
||||
args = arrayOf(action.result.itemCount),
|
||||
),
|
||||
sendEvent(
|
||||
ImportItemsEvent.ShowBasicSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.import_successful.asText(),
|
||||
message = BitwardenPlurals
|
||||
.x_items_have_been_imported_to_your_vault
|
||||
.asPluralsText(
|
||||
quantity = action.result.itemCount,
|
||||
args = arrayOf(action.result.itemCount),
|
||||
),
|
||||
),
|
||||
),
|
||||
relay = SnackbarRelay.LOGINS_IMPORTED,
|
||||
)
|
||||
sendEvent(ImportItemsEvent.NavigateToVault)
|
||||
}
|
||||
|
||||
ImportCredentialsResult.NoItems -> {
|
||||
@ -175,20 +213,25 @@ class ImportItemsViewModel @Inject constructor(
|
||||
|
||||
is ImportCredentialsResult.SyncFailed -> {
|
||||
clearDialogs()
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.vault_sync_failed.asText(),
|
||||
message = BitwardenString
|
||||
.your_items_have_been_successfully_imported_but_could_not_sync_vault
|
||||
.asText(),
|
||||
actionLabel = BitwardenString.try_again.asText(),
|
||||
),
|
||||
relay = SnackbarRelay.VAULT_SYNC_FAILED,
|
||||
)
|
||||
showSyncFailedSnackbar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSyncFailedSnackbar() {
|
||||
sendEvent(
|
||||
ImportItemsEvent.ShowSyncFailedSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.vault_sync_failed.asText(),
|
||||
message = BitwardenString
|
||||
.your_items_have_been_successfully_imported_but_could_not_sync_vault
|
||||
.asText(),
|
||||
actionLabel = BitwardenString.try_again.asText(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearDialogs() {
|
||||
mutableStateFlow.update { it.copy(dialog = null) }
|
||||
}
|
||||
@ -209,7 +252,7 @@ class ImportItemsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateImportProgress(message: Text) {
|
||||
private fun showLoadingDialog(message: Text) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = ImportItemsState.DialogState.Loading(message = message),
|
||||
@ -272,11 +315,6 @@ sealed class ImportItemsAction {
|
||||
val selectionResult: ImportCredentialsSelectionResult,
|
||||
) : ImportItemsAction()
|
||||
|
||||
/**
|
||||
* User clicked the Return to vault button.
|
||||
*/
|
||||
data object ReturnToVaultClick : ImportItemsAction()
|
||||
|
||||
/**
|
||||
* User clicked the back button.
|
||||
*/
|
||||
@ -287,6 +325,11 @@ sealed class ImportItemsAction {
|
||||
*/
|
||||
data object DismissDialog : ImportItemsAction()
|
||||
|
||||
/**
|
||||
* User clicked the try again button.
|
||||
*/
|
||||
data object SyncFailedTryAgainClick : ImportItemsAction()
|
||||
|
||||
/**
|
||||
* Internal actions that the [ImportItemsViewModel] may itself send.
|
||||
*/
|
||||
@ -294,7 +337,16 @@ sealed class ImportItemsAction {
|
||||
/**
|
||||
* Import CXF result received.
|
||||
*/
|
||||
data class ImportCredentialsResultReceive(val result: ImportCredentialsResult) : Internal()
|
||||
data class ImportCredentialsResultReceive(
|
||||
val result: ImportCredentialsResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Retry sync result received.
|
||||
*/
|
||||
data class RetrySyncResultReceive(
|
||||
val result: SyncVaultDataResult,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,11 +365,6 @@ sealed class ImportItemsEvent {
|
||||
*/
|
||||
data object NavigateToImportFromComputer : ImportItemsEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the vault.
|
||||
*/
|
||||
data object NavigateToVault : ImportItemsEvent()
|
||||
|
||||
/**
|
||||
* Show registered import sources.
|
||||
*
|
||||
@ -326,4 +373,18 @@ sealed class ImportItemsEvent {
|
||||
data class ShowRegisteredImportSources(
|
||||
val credentialTypes: List<String>,
|
||||
) : ImportItemsEvent(), BackgroundEvent
|
||||
|
||||
/**
|
||||
* Show a basic snackbar.
|
||||
*/
|
||||
data class ShowBasicSnackbar(
|
||||
val data: BitwardenSnackbarData,
|
||||
) : ImportItemsEvent(), BackgroundEvent
|
||||
|
||||
/**
|
||||
* Show a snackbar indicating that the sync failed, with an option to retry.
|
||||
*/
|
||||
data class ShowSyncFailedSnackbar(
|
||||
val data: BitwardenSnackbarData,
|
||||
) : ImportItemsEvent(), BackgroundEvent
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ data class ImportItemsHandler(
|
||||
val onDismissDialog: () -> Unit,
|
||||
val onImportFromAnotherAppClick: () -> Unit,
|
||||
val onImportFromComputerClick: () -> Unit,
|
||||
val onSyncFailedTryAgainClick: () -> Unit,
|
||||
) {
|
||||
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
@ -30,14 +31,17 @@ data class ImportItemsHandler(
|
||||
onNavigateBack = {
|
||||
viewModel.trySendAction(ImportItemsAction.BackClick)
|
||||
},
|
||||
onDismissDialog = {
|
||||
viewModel.trySendAction(ImportItemsAction.DismissDialog)
|
||||
},
|
||||
onImportFromAnotherAppClick = {
|
||||
viewModel.trySendAction(ImportItemsAction.ImportFromAnotherAppClick)
|
||||
},
|
||||
onImportFromComputerClick = {
|
||||
viewModel.trySendAction(ImportItemsAction.ImportFromComputerClick)
|
||||
},
|
||||
onDismissDialog = {
|
||||
viewModel.trySendAction(ImportItemsAction.DismissDialog)
|
||||
onSyncFailedTryAgainClick = {
|
||||
viewModel.trySendAction(ImportItemsAction.SyncFailedTryAgainClick)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -190,7 +190,6 @@ class VaultViewModel @Inject constructor(
|
||||
SnackbarRelay.CIPHER_RESTORED,
|
||||
SnackbarRelay.CIPHER_UPDATED,
|
||||
SnackbarRelay.LOGINS_IMPORTED,
|
||||
SnackbarRelay.VAULT_SYNC_FAILED,
|
||||
),
|
||||
)
|
||||
.map { VaultAction.Internal.SnackbarDataReceive(it) }
|
||||
|
||||
@ -24,6 +24,7 @@ import org.junit.Test
|
||||
class VaultSettingsScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onNavigateToImportLoginsCalled = false
|
||||
private var onNavigateToImportItemsCalled = false
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToExportVaultCalled = false
|
||||
private var onNavigateToFoldersCalled = false
|
||||
@ -48,6 +49,7 @@ class VaultSettingsScreenTest : BitwardenComposeTest() {
|
||||
onNavigateToExportVault = { onNavigateToExportVaultCalled = true },
|
||||
onNavigateToFolders = { onNavigateToFoldersCalled = true },
|
||||
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
|
||||
onNavigateToImportItems = { onNavigateToImportItemsCalled = true },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.settings.vault
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
|
||||
import com.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FirstTimeActionManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FirstTimeState
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
@ -29,6 +31,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
|
||||
every { firstTimeStateFlow } returns mutableFirstTimeStateFlow
|
||||
every { storeShowImportLoginsSettingsBadge(any()) } just runs
|
||||
}
|
||||
private val featureFlagManager = mockk<FeatureFlagManager>()
|
||||
|
||||
private val mutableSnackbarSharedFlow = bufferedMutableSharedFlow<BitwardenSnackbarData>()
|
||||
private val snackbarRelayManager = mockk<SnackbarRelayManager> {
|
||||
@ -58,17 +61,39 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ImportItemsClick should emit send NavigateToImportVault`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
|
||||
assertEquals(
|
||||
VaultSettingsEvent.NavigateToImportVault,
|
||||
awaitItem(),
|
||||
)
|
||||
fun `ImportItemsClick should emit NavigateToImportVault when CredentialExchangeProtocolImport is disabled`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)
|
||||
} returns false
|
||||
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
|
||||
assertEquals(
|
||||
VaultSettingsEvent.NavigateToImportVault,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `ImportItemsClick should emit NavigateToImportItems when CredentialExchangeProtocolImport is enabled`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
every {
|
||||
featureFlagManager.getFeatureFlag(FlagKey.CredentialExchangeProtocolImport)
|
||||
} returns true
|
||||
viewModel.trySendAction(VaultSettingsAction.ImportItemsClick)
|
||||
assertEquals(
|
||||
VaultSettingsEvent.NavigateToImportItems,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldShowImportCard should update when first time state changes`() = runTest {
|
||||
@ -131,6 +156,7 @@ class VaultSettingsViewModelTest : BaseViewModelTest() {
|
||||
private fun createViewModel(): VaultSettingsViewModel = VaultSettingsViewModel(
|
||||
firstTimeActionManager = firstTimeActionManager,
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
featureFlagManager = featureFlagManager,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -31,7 +31,6 @@ import org.junit.Test
|
||||
class ImportItemsScreenTest : BitwardenComposeTest() {
|
||||
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateToVaultCalled = false
|
||||
private var onNavigateToImportFromComputerCalled = false
|
||||
|
||||
private val credentialExchangeImporter = mockk<CredentialExchangeImporter>()
|
||||
@ -51,7 +50,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() {
|
||||
) {
|
||||
ImportItemsScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateToVault = { onNavigateToVaultCalled = true },
|
||||
onNavigateToImportFromComputer = { onNavigateToImportFromComputerCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
@ -117,12 +115,6 @@ class ImportItemsScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToVault should call onNavigateToVault`() {
|
||||
mockEventFlow.tryEmit(ImportItemsEvent.NavigateToVault)
|
||||
assertTrue(onNavigateToVaultCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ShowRegisteredImportSources should call CredentialExchangeImporter`() = runTest {
|
||||
val importCredentialsSelectionResult = ImportCredentialsSelectionResult.Success(
|
||||
@ -205,4 +197,4 @@ class ImportItemsScreenTest : BitwardenComposeTest() {
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: ImportItemsState = ImportItemsState(dialog = null)
|
||||
private val DEFAULT_STATE: ImportItemsState = ImportItemsState()
|
||||
|
||||
@ -11,17 +11,14 @@ import com.bitwarden.ui.platform.resource.BitwardenPlurals
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.asPluralsText
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.vault.manager.model.SyncVaultDataResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.ImportCredentialsResult
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import io.mockk.awaits
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
@ -29,9 +26,6 @@ import org.junit.jupiter.api.Test
|
||||
class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val vaultRepository = mockk<VaultRepository>()
|
||||
private val snackbarRelayManager = mockk<SnackbarRelayManager> {
|
||||
every { sendSnackbarData(any(), any()) } just runs
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `BackClick sends NavigateBack event`() = runTest {
|
||||
@ -55,7 +49,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
fun `DismissDialog updates state`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ImportItemsAction.DismissDialog)
|
||||
assertEquals(ImportItemsState(dialog = null), viewModel.stateFlow.value)
|
||||
assertEquals(ImportItemsState(), viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -158,14 +152,16 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ReturnToVaultClick sends NavigateToVault event`() = runTest {
|
||||
fun `SyncFailedTryAgainClick should update state and trigger sync`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ImportItemsAction.ReturnToVaultClick)
|
||||
assertEquals(
|
||||
ImportItemsEvent.NavigateToVault,
|
||||
awaitItem(),
|
||||
)
|
||||
coEvery {
|
||||
vaultRepository.syncForResult()
|
||||
} returns SyncVaultDataResult.Success(itemsAvailable = true)
|
||||
|
||||
viewModel.trySendAction(ImportItemsAction.SyncFailedTryAgainClick)
|
||||
|
||||
coVerify(exactly = 1) {
|
||||
vaultRepository.syncForResult()
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +189,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Internal ImportCredentialsResultReceive with Success result should clear dialogs, show snackbar, and navigate to vault`() =
|
||||
fun `Internal ImportCredentialsResultReceive with Success result should clear dialogs, and show snackbar`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
@ -203,25 +199,21 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = ImportItemsState(dialog = null)
|
||||
val expectedState = ImportItemsState()
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.import_successful.asText(),
|
||||
message = BitwardenPlurals
|
||||
.x_items_have_been_imported_to_your_vault
|
||||
.asPluralsText(
|
||||
quantity = 2,
|
||||
args = arrayOf(2),
|
||||
),
|
||||
),
|
||||
relay = SnackbarRelay.LOGINS_IMPORTED,
|
||||
)
|
||||
}
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
ImportItemsEvent.NavigateToVault,
|
||||
ImportItemsEvent.ShowBasicSnackbar(
|
||||
BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.import_successful.asText(),
|
||||
message = BitwardenPlurals
|
||||
.x_items_have_been_imported_to_your_vault
|
||||
.asPluralsText(
|
||||
quantity = 2,
|
||||
args = arrayOf(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
@ -249,7 +241,7 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Internal ImportCxfResultReceive and SyncFailed should clear dialogs, show snackbar, and navigate to vault`() =
|
||||
fun `Internal ImportCxfResultReceive and SyncFailed should clear dialogs and show snackbar`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
@ -261,18 +253,80 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
)
|
||||
|
||||
val expectedState = ImportItemsState(dialog = null)
|
||||
val expectedState = ImportItemsState()
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
coVerify(exactly = 1) {
|
||||
snackbarRelayManager.sendSnackbarData(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.vault_sync_failed.asText(),
|
||||
message = BitwardenString
|
||||
.your_items_have_been_successfully_imported_but_could_not_sync_vault
|
||||
.asText(),
|
||||
actionLabel = BitwardenString.try_again.asText(),
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
ImportItemsEvent.ShowSyncFailedSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.vault_sync_failed.asText(),
|
||||
message = BitwardenString
|
||||
.your_items_have_been_successfully_imported_but_could_not_sync_vault
|
||||
.asText(),
|
||||
actionLabel = BitwardenString.try_again.asText(),
|
||||
),
|
||||
),
|
||||
relay = SnackbarRelay.VAULT_SYNC_FAILED,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Internal RetrySyncResultReceive with Success should clear dialogs and display success snackbar`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
ImportItemsAction.Internal.RetrySyncResultReceive(
|
||||
result = SyncVaultDataResult.Success(itemsAvailable = true),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ImportItemsState(),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
ImportItemsEvent.ShowBasicSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
message = BitwardenString.syncing_complete.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Internal RetrySyncResultReceive with Error should clear dialogs and show SyncFailed snackbar`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
||||
viewModel.trySendAction(
|
||||
ImportItemsAction.Internal.RetrySyncResultReceive(
|
||||
result = SyncVaultDataResult.Error(throwable = RuntimeException("Error")),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
ImportItemsState(),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
assertEquals(
|
||||
ImportItemsEvent.ShowSyncFailedSnackbar(
|
||||
data = BitwardenSnackbarData(
|
||||
messageHeader = BitwardenString.vault_sync_failed.asText(),
|
||||
message = BitwardenString
|
||||
.your_items_have_been_successfully_imported_but_could_not_sync_vault
|
||||
.asText(),
|
||||
actionLabel = BitwardenString.try_again.asText(),
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -280,6 +334,5 @@ class ImportItemsViewModelTest : BaseViewModelTest() {
|
||||
private fun createViewModel(): ImportItemsViewModel = ImportItemsViewModel(
|
||||
vaultRepository = vaultRepository,
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
snackbarRelayManager = snackbarRelayManager,
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user