mirror of
https://github.com/bitwarden/android.git
synced 2025-02-16 11:59:57 +03:00
PM-11179 PM-11180 PM-11181 Add import logins screen and dialogs. (#4067)
This commit is contained in:
parent
86db9bd3fa
commit
bde47d7919
19 changed files with 991 additions and 8 deletions
|
@ -39,6 +39,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
|
|||
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.importLoginsScreenDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.navigateToImportLoginsScreen
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.vaultItemListingDestinationAsRoot
|
||||
|
@ -104,6 +106,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||
},
|
||||
onNavigateToSetupUnlockScreen = { navController.navigateToSetupUnlockScreen() },
|
||||
onNavigateToSetupAutoFillScreen = { navController.navigateToSetupAutoFillScreen() },
|
||||
onNavigateToImportLogins = { navController.navigateToImportLoginsScreen() },
|
||||
)
|
||||
deleteAccountDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
@ -216,5 +219,8 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
|||
navController.popBackStack()
|
||||
},
|
||||
)
|
||||
importLoginsScreenDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
|||
onNavigateToPasswordHistory: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions(
|
||||
route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
|
||||
|
@ -57,6 +58,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
|||
onNavigateToPasswordHistory = onNavigateToPasswordHistory,
|
||||
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
|
||||
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
onNavigateToPasswordHistory: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
|
@ -136,6 +137,7 @@ fun VaultUnlockedNavBarScreen(
|
|||
},
|
||||
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
|
||||
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -165,6 +167,7 @@ private fun VaultUnlockedNavBarScaffold(
|
|||
navigateToPasswordHistory: () -> Unit,
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
) {
|
||||
var shouldDimNavBar by remember { mutableStateOf(false) }
|
||||
|
||||
|
@ -224,6 +227,7 @@ private fun VaultUnlockedNavBarScaffold(
|
|||
onDimBottomNavBarRequest = { shouldDim ->
|
||||
shouldDimNavBar = shouldDim
|
||||
},
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
)
|
||||
sendGraph(
|
||||
navController = navController,
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val IMPORT_LOGINS_ROUTE = "import-logins"
|
||||
|
||||
/**
|
||||
* Helper function to navigate to the import logins screen.
|
||||
*/
|
||||
fun NavController.navigateToImportLoginsScreen(navOptions: NavOptions? = null) {
|
||||
navigate(route = IMPORT_LOGINS_ROUTE, navOptions = navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the import logins screen to the navigation graph.
|
||||
*/
|
||||
fun NavGraphBuilder.importLoginsScreenDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = IMPORT_LOGINS_ROUTE,
|
||||
) {
|
||||
ImportLoginsScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHandler
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
|
||||
|
||||
/**
|
||||
* Top level component for the import logins screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ImportLoginsScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: ImportLoginsViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = rememberImportLoginHandler(viewModel = viewModel)
|
||||
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
ImportLoginsEvent.NavigateBack -> onNavigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
ImportLoginsDialogContent(state = state, handler = handler)
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(R.string.import_logins),
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(R.drawable.ic_close),
|
||||
onNavigationIconClick = handler.onCloseClick,
|
||||
navigationIconContentDescription = stringResource(R.string.close),
|
||||
),
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues = innerPadding),
|
||||
) {
|
||||
ImportLoginsContent(
|
||||
onGetStartedClick = handler.onGetStartedClick,
|
||||
onImportLaterClick = handler.onImportLaterClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportLoginsDialogContent(
|
||||
state: ImportLoginsState,
|
||||
handler: ImportLoginHandler,
|
||||
) {
|
||||
val confirmButtonText = stringResource(R.string.confirm)
|
||||
val dismissButtonText = stringResource(R.string.cancel)
|
||||
when (val dialogState = state.dialogState) {
|
||||
ImportLoginsState.DialogState.GetStarted -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialogState.title(),
|
||||
message = dialogState.message(),
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
confirmButtonText = confirmButtonText,
|
||||
dismissButtonText = dismissButtonText,
|
||||
onConfirmClick = handler.onConfirmGetStarted,
|
||||
onDismissClick = handler.onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
ImportLoginsState.DialogState.ImportLater -> {
|
||||
BitwardenTwoButtonDialog(
|
||||
title = dialogState.title(),
|
||||
message = dialogState.message(),
|
||||
onDismissRequest = handler.onDismissDialog,
|
||||
confirmButtonText = confirmButtonText,
|
||||
dismissButtonText = dismissButtonText,
|
||||
onConfirmClick = handler.onConfirmImportLater,
|
||||
onDismissClick = handler.onDismissDialog,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportLoginsContent(
|
||||
onGetStartedClick: () -> Unit,
|
||||
onImportLaterClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
.padding(top = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = rememberVectorPainter(R.drawable.img_import_logins),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.size(124.dp),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.give_your_vault_a_head_start),
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
R.string.from_your_computer_follow_these_instructions_to_export_saved_passwords,
|
||||
),
|
||||
style = BitwardenTheme.typography.bodyMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(R.string.get_started),
|
||||
onClick = onGetStartedClick,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
BitwardenOutlinedButton(
|
||||
label = stringResource(R.string.import_logins_later),
|
||||
onClick = onImportLaterClick,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ImportLoginsInitialContent_preview() {
|
||||
BitwardenTheme {
|
||||
Column(
|
||||
modifier = Modifier.background(
|
||||
BitwardenTheme.colorScheme.background.primary,
|
||||
),
|
||||
) {
|
||||
ImportLoginsContent(
|
||||
onGetStartedClick = {},
|
||||
onImportLaterClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ImportLoginsScreenDialog_preview(
|
||||
@PreviewParameter(ImportLoginsDialogContentPreviewProvider::class) state: ImportLoginsState,
|
||||
) {
|
||||
BitwardenTheme {
|
||||
Column(
|
||||
modifier = Modifier.background(
|
||||
BitwardenTheme.colorScheme.background.primary,
|
||||
),
|
||||
) {
|
||||
ImportLoginsDialogContent(
|
||||
state = state,
|
||||
handler = ImportLoginHandler(
|
||||
onDismissDialog = {},
|
||||
onConfirmGetStarted = {},
|
||||
onConfirmImportLater = {},
|
||||
onCloseClick = {},
|
||||
onGetStartedClick = {},
|
||||
onImportLaterClick = {},
|
||||
),
|
||||
)
|
||||
ImportLoginsContent(
|
||||
onGetStartedClick = {},
|
||||
onImportLaterClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OmitFromCoverage
|
||||
private class ImportLoginsDialogContentPreviewProvider :
|
||||
PreviewParameterProvider<ImportLoginsState> {
|
||||
override val values: Sequence<ImportLoginsState>
|
||||
get() = sequenceOf(
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||
),
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||
),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* View model for the [ImportLoginsScreen].
|
||||
*/
|
||||
@HiltViewModel
|
||||
class ImportLoginsViewModel @Inject constructor() :
|
||||
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
|
||||
initialState = ImportLoginsState(
|
||||
null,
|
||||
),
|
||||
) {
|
||||
override fun handleAction(action: ImportLoginsAction) {
|
||||
when (action) {
|
||||
ImportLoginsAction.ConfirmGetStarted -> handleConfirmGetStarted()
|
||||
ImportLoginsAction.ConfirmImportLater -> handleConfirmImportLater()
|
||||
ImportLoginsAction.DismissDialog -> handleDismissDialog()
|
||||
ImportLoginsAction.GetStartedClick -> handleGetStartedClick()
|
||||
ImportLoginsAction.ImportLaterClick -> handleImportLaterClick()
|
||||
ImportLoginsAction.CloseClick -> handleCloseClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
sendEvent(ImportLoginsEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleImportLaterClick() {
|
||||
updateDialogState(ImportLoginsState.DialogState.ImportLater)
|
||||
}
|
||||
|
||||
private fun handleGetStartedClick() {
|
||||
updateDialogState(ImportLoginsState.DialogState.GetStarted)
|
||||
}
|
||||
|
||||
private fun handleDismissDialog() {
|
||||
dismissDialog()
|
||||
}
|
||||
|
||||
private fun handleConfirmImportLater() {
|
||||
dismissDialog()
|
||||
sendEvent(ImportLoginsEvent.NavigateBack)
|
||||
}
|
||||
|
||||
private fun handleConfirmGetStarted() {
|
||||
dismissDialog()
|
||||
// TODO - PM-11182: Move to first step in instructions.
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
updateDialogState(null)
|
||||
}
|
||||
|
||||
private fun updateDialogState(dialogState: ImportLoginsState.DialogState?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = dialogState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model state for the [ImportLoginsViewModel].
|
||||
*/
|
||||
data class ImportLoginsState(
|
||||
val dialogState: DialogState?,
|
||||
) {
|
||||
/**
|
||||
* Dialog states for the [ImportLoginsViewModel].
|
||||
*/
|
||||
sealed class DialogState {
|
||||
abstract val message: Text
|
||||
abstract val title: Text
|
||||
|
||||
/**
|
||||
* Import logins later dialog state.
|
||||
*/
|
||||
data object ImportLater : DialogState() {
|
||||
override val message: Text =
|
||||
R.string.you_can_return_to_complete_this_step_anytime_from_settings.asText()
|
||||
override val title: Text = R.string.import_logins_later_dialog_title.asText()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get started dialog state.
|
||||
*/
|
||||
data object GetStarted : DialogState() {
|
||||
override val message: Text =
|
||||
R.string.the_following_instructions_will_guide_you_through_importing_logins.asText()
|
||||
override val title: Text = R.string.do_you_have_a_computer_available.asText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Model events that can be sent from the [ImportLoginsViewModel]
|
||||
*/
|
||||
sealed class ImportLoginsEvent {
|
||||
/**
|
||||
* Navigate back to the previous screen.
|
||||
*/
|
||||
data object NavigateBack : ImportLoginsEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Model actions that can be handled by the [ImportLoginsViewModel].
|
||||
*/
|
||||
sealed class ImportLoginsAction {
|
||||
|
||||
/**
|
||||
* User has clicked the "Get Started" button.
|
||||
*/
|
||||
data object GetStartedClick : ImportLoginsAction()
|
||||
|
||||
/**
|
||||
* User has clicked the "Import Later" button.
|
||||
*/
|
||||
data object ImportLaterClick : ImportLoginsAction()
|
||||
|
||||
/**
|
||||
* User has clicked the "Close" button on the dialog or outside the dialog.
|
||||
*/
|
||||
data object DismissDialog : ImportLoginsAction()
|
||||
|
||||
/**
|
||||
* User has confirmed the "Import Later" dialog.
|
||||
*/
|
||||
data object ConfirmImportLater : ImportLoginsAction()
|
||||
|
||||
/**
|
||||
* User has confirmed the "Get Started" dialog.
|
||||
*/
|
||||
data object ConfirmGetStarted : ImportLoginsAction()
|
||||
|
||||
/**
|
||||
* User has clicked the "Close" icon button.
|
||||
*/
|
||||
data object CloseClick : ImportLoginsAction()
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsViewModel
|
||||
|
||||
/**
|
||||
* Action handlers for the [ImportLoginsScreen].
|
||||
*/
|
||||
data class ImportLoginHandler(
|
||||
val onGetStartedClick: () -> Unit,
|
||||
val onImportLaterClick: () -> Unit,
|
||||
val onDismissDialog: () -> Unit,
|
||||
val onConfirmGetStarted: () -> Unit,
|
||||
val onConfirmImportLater: () -> Unit,
|
||||
val onCloseClick: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Creates an instance of [ImportLoginHandler] using the provided [ImportLoginsViewModel].
|
||||
*/
|
||||
fun create(viewModel: ImportLoginsViewModel) = ImportLoginHandler(
|
||||
onGetStartedClick = { viewModel.trySendAction(ImportLoginsAction.GetStartedClick) },
|
||||
onImportLaterClick = { viewModel.trySendAction(ImportLoginsAction.ImportLaterClick) },
|
||||
onDismissDialog = { viewModel.trySendAction(ImportLoginsAction.DismissDialog) },
|
||||
onConfirmGetStarted = { viewModel.trySendAction(ImportLoginsAction.ConfirmGetStarted) },
|
||||
onConfirmImportLater = {
|
||||
viewModel.trySendAction(ImportLoginsAction.ConfirmImportLater)
|
||||
},
|
||||
onCloseClick = { viewModel.trySendAction(ImportLoginsAction.CloseClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to remember a [ImportLoginHandler] instance in a [Composable] scope.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberImportLoginHandler(viewModel: ImportLoginsViewModel): ImportLoginHandler =
|
||||
remember(viewModel) {
|
||||
ImportLoginHandler.create(viewModel = viewModel)
|
||||
}
|
|
@ -24,6 +24,7 @@ fun NavGraphBuilder.vaultGraph(
|
|||
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
route = VAULT_GRAPH_ROUTE,
|
||||
|
@ -41,6 +42,7 @@ fun NavGraphBuilder.vaultGraph(
|
|||
},
|
||||
onNavigateToSearchVault = onNavigateToSearchVault,
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
)
|
||||
vaultItemListingDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
|
|
@ -21,6 +21,7 @@ fun NavGraphBuilder.vaultDestination(
|
|||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
) {
|
||||
composableWithRootPushTransitions(
|
||||
route = VAULT_ROUTE,
|
||||
|
@ -33,6 +34,7 @@ fun NavGraphBuilder.vaultDestination(
|
|||
onNavigateToVerificationCodeScreen = onNavigateToVerificationCodeScreen,
|
||||
onNavigateToSearchVault = onNavigateToSearchVault,
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ fun VaultScreen(
|
|||
onNavigateToVaultItemListingScreen: (vaultItemType: VaultItemListingType) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: () -> Unit,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||
|
@ -122,6 +123,8 @@ fun VaultScreen(
|
|||
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
VaultEvent.NavigateToImportLogins -> onNavigateToImportLogins()
|
||||
}
|
||||
}
|
||||
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
|
||||
|
|
|
@ -181,7 +181,7 @@ class VaultViewModel @Inject constructor(
|
|||
|
||||
private fun handleImportActionCardClick() {
|
||||
dismissImportLoginCard()
|
||||
// TODO: PM-11179 - navigate to import logins screen
|
||||
sendEvent(VaultEvent.NavigateToImportLogins)
|
||||
}
|
||||
|
||||
private fun handleDismissImportActionCard() {
|
||||
|
@ -1004,6 +1004,11 @@ sealed class VaultEvent {
|
|||
*/
|
||||
data object NavigateOutOfApp : VaultEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the import logins screen.
|
||||
*/
|
||||
data object NavigateToImportLogins : VaultEvent()
|
||||
|
||||
/**
|
||||
* Show a toast with the given [message].
|
||||
*/
|
||||
|
|
73
app/src/main/res/drawable-night/img_import_logins.xml
Normal file
73
app/src/main/res/drawable-night/img_import_logins.xml
Normal file
|
@ -0,0 +1,73 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="201dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="201">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0.67h200v200h-200z"/>
|
||||
<path
|
||||
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H154.17C161.07,25.67 166.67,31.26 166.67,38.17V138.17C166.67,145.07 161.07,150.67 154.17,150.67H12.5C5.6,150.67 0,145.07 0,138.17V38.17Z"
|
||||
android:fillColor="#79A1E9"/>
|
||||
<path
|
||||
android:pathData="M154.17,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V138.17C4.17,142.77 7.9,146.5 12.5,146.5H154.17C158.77,146.5 162.5,142.77 162.5,138.17V38.17C162.5,33.56 158.77,29.83 154.17,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V138.17C0,145.07 5.6,150.67 12.5,150.67H154.17C161.07,150.67 166.67,145.07 166.67,138.17V38.17C166.67,31.26 161.07,25.67 154.17,25.67H12.5Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M164.58,50.67L2.08,50.67L2.08,46.5L164.58,46.5L164.58,50.67Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M158.33,38.17C158.33,40.47 156.47,42.33 154.17,42.33C151.87,42.33 150,40.47 150,38.17C150,35.87 151.87,34 154.17,34C156.47,34 158.33,35.87 158.33,38.17Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M145.83,38.17C145.83,40.47 143.97,42.33 141.67,42.33C139.37,42.33 137.5,40.47 137.5,38.17C137.5,35.87 139.37,34 141.67,34C143.97,34 145.83,35.87 145.83,38.17Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M133.33,38.17C133.33,40.47 131.47,42.33 129.17,42.33C126.86,42.33 125,40.47 125,38.17C125,35.87 126.86,34 129.17,34C131.47,34 133.33,35.87 133.33,38.17Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M125,67.33C125,60.43 130.6,54.83 137.5,54.83H187.5C194.4,54.83 200,60.43 200,67.33V175.67C200,182.57 194.4,188.17 187.5,188.17H137.5C130.6,188.17 125,182.57 125,175.67V67.33Z"
|
||||
android:fillColor="#AAC3EF"/>
|
||||
<path
|
||||
android:pathData="M187.5,59H137.5C132.9,59 129.17,62.73 129.17,67.33V175.67C129.17,180.27 132.9,184 137.5,184H187.5C192.1,184 195.83,180.27 195.83,175.67V67.33C195.83,62.73 192.1,59 187.5,59ZM137.5,54.83C130.6,54.83 125,60.43 125,67.33V175.67C125,182.57 130.6,188.17 137.5,188.17H187.5C194.4,188.17 200,182.57 200,175.67V67.33C200,60.43 194.4,54.83 187.5,54.83H137.5Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M152.08,67.33C152.08,66.18 153.02,65.25 154.17,65.25H170.83C171.99,65.25 172.92,66.18 172.92,67.33C172.92,68.48 171.99,69.42 170.83,69.42H154.17C153.02,69.42 152.08,68.48 152.08,67.33Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M141.67,173.58C141.67,170.13 144.46,167.33 147.92,167.33H177.08C180.53,167.33 183.33,170.13 183.33,173.58V194.42C183.33,197.87 180.53,200.67 177.08,200.67H147.92C144.46,200.67 141.67,197.87 141.67,194.42V173.58Z"
|
||||
android:fillColor="#FFBF00"/>
|
||||
<path
|
||||
android:pathData="M177.08,171.5H147.92C146.76,171.5 145.83,172.43 145.83,173.58V194.42C145.83,195.57 146.76,196.5 147.92,196.5H177.08C178.23,196.5 179.17,195.57 179.17,194.42V173.58C179.17,172.43 178.23,171.5 177.08,171.5ZM147.92,167.33C144.46,167.33 141.67,170.13 141.67,173.58V194.42C141.67,197.87 144.46,200.67 147.92,200.67H177.08C180.53,200.67 183.33,197.87 183.33,194.42V173.58C183.33,170.13 180.53,167.33 177.08,167.33H147.92Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M160.42,179.83C160.42,178.68 161.35,177.75 162.5,177.75C163.65,177.75 164.58,178.68 164.58,179.83V188.17C164.58,189.32 163.65,190.25 162.5,190.25C161.35,190.25 160.42,189.32 160.42,188.17V179.83Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M152.08,165.25C152.08,159.5 156.75,154.83 162.5,154.83C168.25,154.83 172.92,159.5 172.92,165.25V167.33H168.75V165.25C168.75,161.8 165.95,159 162.5,159C159.05,159 156.25,161.8 156.25,165.25V167.33H152.08V165.25Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.33,111.08C84.48,111.08 85.42,112.02 85.42,113.17V125.67C85.42,129.12 88.21,131.92 91.67,131.92H159.55L150.61,122.97C149.8,122.16 149.8,120.84 150.61,120.03C151.42,119.21 152.74,119.21 153.56,120.03L166.06,132.53C166.87,133.34 166.87,134.66 166.06,135.47L153.56,147.97C152.74,148.79 151.42,148.79 150.61,147.97C149.8,147.16 149.8,145.84 150.61,145.03L159.55,136.08H91.67C85.91,136.08 81.25,131.42 81.25,125.67V113.17C81.25,112.02 82.18,111.08 83.33,111.08Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M54.17,81.92C54.17,78.46 56.96,75.67 60.42,75.67H106.25C109.7,75.67 112.5,78.46 112.5,81.92V115.25C112.5,118.7 109.7,121.5 106.25,121.5H60.42C56.96,121.5 54.17,118.7 54.17,115.25V81.92Z"
|
||||
android:fillColor="#F3F6F9"/>
|
||||
<path
|
||||
android:pathData="M106.25,79.83H60.42C59.27,79.83 58.33,80.77 58.33,81.92V115.25C58.33,116.4 59.27,117.33 60.42,117.33H106.25C107.4,117.33 108.33,116.4 108.33,115.25V81.92C108.33,80.77 107.4,79.83 106.25,79.83ZM60.42,75.67C56.96,75.67 54.17,78.46 54.17,81.92V115.25C54.17,118.7 56.96,121.5 60.42,121.5H106.25C109.7,121.5 112.5,118.7 112.5,115.25V81.92C112.5,78.46 109.7,75.67 106.25,75.67H60.42Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M81.25,92.33C81.25,91.18 82.18,90.25 83.33,90.25C84.48,90.25 85.42,91.18 85.42,92.33V104.83C85.42,105.98 84.48,106.92 83.33,106.92C82.18,106.92 81.25,105.98 81.25,104.83V92.33Z"
|
||||
android:fillColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M66.67,71.5C66.67,62.29 74.13,54.83 83.33,54.83C92.54,54.83 100,62.29 100,71.5V75.67H95.83V71.5C95.83,64.6 90.24,59 83.33,59C76.43,59 70.83,64.6 70.83,71.5V75.67H66.67V71.5Z"
|
||||
android:fillColor="#175DDC"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
</vector>
|
73
app/src/main/res/drawable/img_import_logins.xml
Normal file
73
app/src/main/res/drawable/img_import_logins.xml
Normal file
|
@ -0,0 +1,73 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="201dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="201">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0.67h200v200h-200z"/>
|
||||
<path
|
||||
android:pathData="M0,38.17C0,31.26 5.6,25.67 12.5,25.67H154.17C161.07,25.67 166.67,31.26 166.67,38.17V138.17C166.67,145.07 161.07,150.67 154.17,150.67H12.5C5.6,150.67 0,145.07 0,138.17V38.17Z"
|
||||
android:fillColor="#AAC3EF"/>
|
||||
<path
|
||||
android:pathData="M154.17,29.83H12.5C7.9,29.83 4.17,33.56 4.17,38.17V138.17C4.17,142.77 7.9,146.5 12.5,146.5H154.17C158.77,146.5 162.5,142.77 162.5,138.17V38.17C162.5,33.56 158.77,29.83 154.17,29.83ZM12.5,25.67C5.6,25.67 0,31.26 0,38.17V138.17C0,145.07 5.6,150.67 12.5,150.67H154.17C161.07,150.67 166.67,145.07 166.67,138.17V38.17C166.67,31.26 161.07,25.67 154.17,25.67H12.5Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M164.58,50.67L2.08,50.67L2.08,46.5L164.58,46.5L164.58,50.67Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M158.33,38.17C158.33,40.47 156.47,42.33 154.17,42.33C151.87,42.33 150,40.47 150,38.17C150,35.87 151.87,34 154.17,34C156.47,34 158.33,35.87 158.33,38.17Z"
|
||||
android:fillColor="#020F66"/>
|
||||
<path
|
||||
android:pathData="M145.83,38.17C145.83,40.47 143.97,42.33 141.67,42.33C139.37,42.33 137.5,40.47 137.5,38.17C137.5,35.87 139.37,34 141.67,34C143.97,34 145.83,35.87 145.83,38.17Z"
|
||||
android:fillColor="#020F66"/>
|
||||
<path
|
||||
android:pathData="M133.33,38.17C133.33,40.47 131.47,42.33 129.17,42.33C126.86,42.33 125,40.47 125,38.17C125,35.87 126.86,34 129.17,34C131.47,34 133.33,35.87 133.33,38.17Z"
|
||||
android:fillColor="#020F66"/>
|
||||
<path
|
||||
android:pathData="M125,67.33C125,60.43 130.6,54.83 137.5,54.83H187.5C194.4,54.83 200,60.43 200,67.33V175.67C200,182.57 194.4,188.17 187.5,188.17H137.5C130.6,188.17 125,182.57 125,175.67V67.33Z"
|
||||
android:fillColor="#DBE5F6"/>
|
||||
<path
|
||||
android:pathData="M187.5,59H137.5C132.9,59 129.17,62.73 129.17,67.33V175.67C129.17,180.27 132.9,184 137.5,184H187.5C192.1,184 195.83,180.27 195.83,175.67V67.33C195.83,62.73 192.1,59 187.5,59ZM137.5,54.83C130.6,54.83 125,60.43 125,67.33V175.67C125,182.57 130.6,188.17 137.5,188.17H187.5C194.4,188.17 200,182.57 200,175.67V67.33C200,60.43 194.4,54.83 187.5,54.83H137.5Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M152.08,67.33C152.08,66.18 153.02,65.25 154.17,65.25H170.83C171.99,65.25 172.92,66.18 172.92,67.33C172.92,68.48 171.99,69.42 170.83,69.42H154.17C153.02,69.42 152.08,68.48 152.08,67.33Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M141.67,173.58C141.67,170.13 144.46,167.33 147.92,167.33H177.08C180.53,167.33 183.33,170.13 183.33,173.58V194.42C183.33,197.87 180.53,200.67 177.08,200.67H147.92C144.46,200.67 141.67,197.87 141.67,194.42V173.58Z"
|
||||
android:fillColor="#FFBF00"/>
|
||||
<path
|
||||
android:pathData="M177.08,171.5H147.92C146.76,171.5 145.83,172.43 145.83,173.58V194.42C145.83,195.57 146.76,196.5 147.92,196.5H177.08C178.23,196.5 179.17,195.57 179.17,194.42V173.58C179.17,172.43 178.23,171.5 177.08,171.5ZM147.92,167.33C144.46,167.33 141.67,170.13 141.67,173.58V194.42C141.67,197.87 144.46,200.67 147.92,200.67H177.08C180.53,200.67 183.33,197.87 183.33,194.42V173.58C183.33,170.13 180.53,167.33 177.08,167.33H147.92Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M160.42,179.83C160.42,178.68 161.35,177.75 162.5,177.75C163.65,177.75 164.58,178.68 164.58,179.83V188.17C164.58,189.32 163.65,190.25 162.5,190.25C161.35,190.25 160.42,189.32 160.42,188.17V179.83Z"
|
||||
android:fillColor="#020F66"/>
|
||||
<path
|
||||
android:pathData="M152.08,165.25C152.08,159.5 156.75,154.83 162.5,154.83C168.25,154.83 172.92,159.5 172.92,165.25V167.33H168.75V165.25C168.75,161.8 165.95,159 162.5,159C159.05,159 156.25,161.8 156.25,165.25V167.33H152.08V165.25Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.33,111.08C84.48,111.08 85.42,112.02 85.42,113.17V125.67C85.42,129.12 88.21,131.92 91.67,131.92H159.55L150.61,122.97C149.8,122.16 149.8,120.84 150.61,120.03C151.42,119.21 152.74,119.21 153.56,120.03L166.06,132.53C166.87,133.34 166.87,134.66 166.06,135.47L153.56,147.97C152.74,148.79 151.42,148.79 150.61,147.97C149.8,147.16 149.8,145.84 150.61,145.03L159.55,136.08H91.67C85.91,136.08 81.25,131.42 81.25,125.67V113.17C81.25,112.02 82.18,111.08 83.33,111.08Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M54.17,81.92C54.17,78.46 56.96,75.67 60.42,75.67H106.25C109.7,75.67 112.5,78.46 112.5,81.92V115.25C112.5,118.7 109.7,121.5 106.25,121.5H60.42C56.96,121.5 54.17,118.7 54.17,115.25V81.92Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
<path
|
||||
android:pathData="M106.25,79.83H60.42C59.27,79.83 58.33,80.77 58.33,81.92V115.25C58.33,116.4 59.27,117.33 60.42,117.33H106.25C107.4,117.33 108.33,116.4 108.33,115.25V81.92C108.33,80.77 107.4,79.83 106.25,79.83ZM60.42,75.67C56.96,75.67 54.17,78.46 54.17,81.92V115.25C54.17,118.7 56.96,121.5 60.42,121.5H106.25C109.7,121.5 112.5,118.7 112.5,115.25V81.92C112.5,78.46 109.7,75.67 106.25,75.67H60.42Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M81.25,92.33C81.25,91.18 82.18,90.25 83.33,90.25C84.48,90.25 85.42,91.18 85.42,92.33V104.83C85.42,105.98 84.48,106.92 83.33,106.92C82.18,106.92 81.25,105.98 81.25,104.83V92.33Z"
|
||||
android:fillColor="#020F66"/>
|
||||
<path
|
||||
android:pathData="M66.67,71.5C66.67,62.29 74.13,54.83 83.33,54.83C92.54,54.83 100,62.29 100,71.5V75.67H95.83V71.5C95.83,64.6 90.24,59 83.33,59C76.43,59 70.83,64.6 70.83,71.5V75.67H66.67V71.5Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -1020,4 +1020,12 @@ Do you want to switch to this account?</string>
|
|||
<string name="send_sensitive_information_safely">Send sensitive information, safely</string>
|
||||
<string name="import_saved_logins">Import saved logins</string>
|
||||
<string name="use_a_computer_to_import_logins">Use a computer to import logins from an existing password manager</string>
|
||||
<string name="you_can_return_to_complete_this_step_anytime_from_settings">You can return to complete this step anytime in Vault Settings.</string>
|
||||
<string name="import_logins_later">Import logins later</string>
|
||||
<string name="import_logins_later_dialog_title">Import logins later?</string>
|
||||
<string name="do_you_have_a_computer_available">Do you have a computer available?</string>
|
||||
<string name="the_following_instructions_will_guide_you_through_importing_logins">The following instructions will guide you through importing logins from your desktop or laptop computer</string>
|
||||
<string name="import_logins">Import Logins</string>
|
||||
<string name="from_your_computer_follow_these_instructions_to_export_saved_passwords">From your computer, follow these instructions to export saved passwords from your browser or other password manager. Then, safely import them to Bitwarden.</string>
|
||||
<string name="give_your_vault_a_head_start">Give your vault a head start</string>
|
||||
</resources>
|
||||
|
|
|
@ -55,6 +55,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
|||
onNavigateToSearchSend = {},
|
||||
onNavigateToSetupAutoFillScreen = {},
|
||||
onNavigateToSetupUnlockScreen = {},
|
||||
onNavigateToImportLogins = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class ImportLoginsScreenTest : BaseComposeTest() {
|
||||
|
||||
private var navigateBackCalled = false
|
||||
|
||||
private val mutableImportLoginsStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableImportLoginsEventFlow = bufferedMutableSharedFlow<ImportLoginsEvent>()
|
||||
private val viewModel = mockk<ImportLoginsViewModel> {
|
||||
every { eventFlow } returns mutableImportLoginsEventFlow
|
||||
every { stateFlow } returns mutableImportLoginsStateFlow
|
||||
every { trySendAction(any()) } just runs
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
ImportLoginsScreen(
|
||||
onNavigateBack = { navigateBackCalled = true },
|
||||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `navigate back when event is NavigateBack`() {
|
||||
mutableImportLoginsEventFlow.tryEmit(ImportLoginsEvent.NavigateBack)
|
||||
|
||||
assertTrue(navigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when close icon clicked, CloseClick action is sent`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Close")
|
||||
.performClick()
|
||||
|
||||
verifyActionSent(ImportLoginsAction.CloseClick)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when get started clicked, GetStartedClick action is sent`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Get started")
|
||||
.performClick()
|
||||
|
||||
verifyActionSent(ImportLoginsAction.GetStartedClick)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when import later clicked, ImportLaterClick action is sent`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Import logins later")
|
||||
.performClick()
|
||||
|
||||
verifyActionSent(ImportLoginsAction.ImportLaterClick)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dialog content is shown when state updates and is hidden when null`() {
|
||||
mutableImportLoginsStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
|
||||
mutableImportLoginsStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = null,
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.assertNoDialogExists()
|
||||
|
||||
mutableImportLoginsStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||
),
|
||||
)
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when dialog state is GetStarted, GetStarted dialog is shown and sends correct actions when clicked`() {
|
||||
mutableImportLoginsStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Do you have a computer available?", useUnmergedTree = true)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Confirm")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verifyActionSent(ImportLoginsAction.ConfirmGetStarted)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verifyActionSent(ImportLoginsAction.DismissDialog)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when dialog state is ImportLater, ImportLater dialog is shown and sends correct actions when clicked`() {
|
||||
mutableImportLoginsStateFlow.tryEmit(
|
||||
DEFAULT_STATE.copy(
|
||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||
),
|
||||
)
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Import logins later?", useUnmergedTree = true)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Confirm")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verifyActionSent(ImportLoginsAction.ConfirmImportLater)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
verifyActionSent(ImportLoginsAction.DismissDialog)
|
||||
}
|
||||
|
||||
private fun verifyActionSent(action: ImportLoginsAction) {
|
||||
verify { viewModel.trySendAction(action) }
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = ImportLoginsState(dialogState = null)
|
|
@ -0,0 +1,141 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||
|
||||
import app.cash.turbine.test
|
||||
import app.cash.turbine.turbineScope
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Test
|
||||
fun `initial state is correct`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GetStartedClick sets dialog state to GetStarted`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ImportLoginsAction.GetStartedClick)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ImportLaterClick sets dialog state to ImportLater`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(ImportLoginsAction.ImportLaterClick)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissDialog sets dialog state to null`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(
|
||||
DEFAULT_STATE,
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(ImportLoginsAction.GetStartedClick)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(ImportLoginsAction.DismissDialog)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConfirmImportLater sets dialog state to null and sends NavigateBack event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
turbineScope {
|
||||
val stateFlow = viewModel.stateFlow.testIn(backgroundScope)
|
||||
val eventFlow = viewModel.eventFlow.testIn(backgroundScope)
|
||||
// Initial state
|
||||
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
|
||||
|
||||
// Set the dialog state to ImportLater
|
||||
viewModel.trySendAction(ImportLoginsAction.ImportLaterClick)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(ImportLoginsAction.ConfirmImportLater)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
),
|
||||
stateFlow.awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
ImportLoginsEvent.NavigateBack,
|
||||
eventFlow.awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ConfirmGetStarted sets dialog state to null`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
// Initial state
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
|
||||
// Set the dialog state to GetStarted
|
||||
viewModel.trySendAction(ImportLoginsAction.GetStartedClick)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
viewModel.trySendAction(ImportLoginsAction.ConfirmGetStarted)
|
||||
assertEquals(
|
||||
ImportLoginsState(
|
||||
dialogState = null,
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick sends NavigateBack event`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(ImportLoginsAction.CloseClick)
|
||||
assertEquals(
|
||||
ImportLoginsEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel()
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE = ImportLoginsState(dialogState = null)
|
|
@ -60,7 +60,7 @@ import org.junit.Test
|
|||
|
||||
@Suppress("LargeClass")
|
||||
class VaultScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateToImportLoginsCalled = false
|
||||
private var onNavigateToVaultAddItemScreenCalled = false
|
||||
private var onNavigateToVaultItemId: String? = null
|
||||
private var onNavigateToVaultEditItemId: String? = null
|
||||
|
@ -91,6 +91,7 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
onDimBottomNavBarRequest = { onDimBottomNavBarRequestCalled = true },
|
||||
onNavigateToVerificationCodeScreen = { onNavigateToVerificationCodeScreen = true },
|
||||
onNavigateToSearchVault = { onNavigateToSearchScreen = true },
|
||||
onNavigateToImportLogins = { onNavigateToImportLoginsCalled = true },
|
||||
exitManager = exitManager,
|
||||
intentManager = intentManager,
|
||||
permissionsManager = permissionsManager,
|
||||
|
@ -1194,6 +1195,12 @@ class VaultScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultAction.DismissImportActionCard) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when NavigateToImportLogins is sent, it should call onNavigateToImportLogins`() {
|
||||
mutableEventFlow.tryEmit(VaultEvent.NavigateToImportLogins)
|
||||
assertTrue(onNavigateToImportLoginsCalled)
|
||||
}
|
||||
}
|
||||
|
||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
|
|
|
@ -1576,14 +1576,19 @@ class VaultViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `when ImportActionCardClick is sent, repository called to set value to false`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(VaultAction.ImportActionCardClick)
|
||||
verify(exactly = 1) {
|
||||
authRepository.setShowImportLogins(false)
|
||||
fun `when ImportActionCardClick is sent, repository called to set value to false and NavigateToImportLogins event is sent`() =
|
||||
runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAction.ImportActionCardClick)
|
||||
assertEquals(VaultEvent.NavigateToImportLogins, awaitItem())
|
||||
}
|
||||
verify(exactly = 1) {
|
||||
authRepository.setShowImportLogins(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
|
|
Loading…
Add table
Reference in a new issue