mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-11187 show import success bottom sheet after success import sync (#4125)
This commit is contained in:
parent
6f535c0abe
commit
1c10a94109
21 changed files with 701 additions and 131 deletions
|
@ -19,6 +19,7 @@ import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
|
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
|
||||||
|
@ -88,6 +89,7 @@ fun BitwardenTopAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
dividerStyle: TopAppBarDividerStyle = TopAppBarDividerStyle.ON_SCROLL,
|
dividerStyle: TopAppBarDividerStyle = TopAppBarDividerStyle.ON_SCROLL,
|
||||||
actions: @Composable RowScope.() -> Unit = {},
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
minimunHeight: Dp = 48.dp,
|
||||||
) {
|
) {
|
||||||
var titleTextHasOverflow by remember {
|
var titleTextHasOverflow by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
|
@ -131,7 +133,7 @@ fun BitwardenTopAppBar(
|
||||||
colors = bitwardenTopAppBarColors(),
|
colors = bitwardenTopAppBarColors(),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = navigationIconContent,
|
navigationIcon = navigationIconContent,
|
||||||
collapsedHeight = 48.dp,
|
collapsedHeight = minimunHeight,
|
||||||
expandedHeight = 96.dp,
|
expandedHeight = 96.dp,
|
||||||
title = {
|
title = {
|
||||||
// The height of the component is controlled and will only allow for 1 extra row,
|
// The height of the component is controlled and will only allow for 1 extra row,
|
||||||
|
@ -151,7 +153,7 @@ fun BitwardenTopAppBar(
|
||||||
colors = bitwardenTopAppBarColors(),
|
colors = bitwardenTopAppBarColors(),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
navigationIcon = navigationIconContent,
|
navigationIcon = navigationIconContent,
|
||||||
expandedHeight = 48.dp,
|
expandedHeight = minimunHeight,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components.bottomsheet
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
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.scaffold.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable modal bottom sheet that applies provides a bottom sheet layout with the
|
||||||
|
* standard [BitwardenScaffold] and [BitwardenTopAppBar] and expected scrolling behavior with
|
||||||
|
* passed in [sheetContent]
|
||||||
|
*
|
||||||
|
* @param sheetTitle The title to display in the [BitwardenTopAppBar]
|
||||||
|
* @param onDismiss The action to perform when the bottom sheet is dismissed will also be performed
|
||||||
|
* when the "close" icon is clicked, caller must handle any desired animation or hiding of the
|
||||||
|
* bottom sheet.
|
||||||
|
* @param showBottomSheet Whether or not to show the bottom sheet, by default this is true assuming
|
||||||
|
* the showing/hiding will be handled by the caller.
|
||||||
|
* @param sheetContent Content to display in the bottom sheet. The content is passed the padding
|
||||||
|
* from the containing [BitwardenScaffold].
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun BitwardenModalBottomSheet(
|
||||||
|
sheetTitle: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showBottomSheet: Boolean = true,
|
||||||
|
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||||
|
sheetContent: @Composable (PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
if (!showBottomSheet) return
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
modifier = modifier,
|
||||||
|
dragHandle = null,
|
||||||
|
sheetState = sheetState,
|
||||||
|
contentWindowInsets = {
|
||||||
|
WindowInsets(left = 0, top = 0, right = 0, bottom = 0)
|
||||||
|
},
|
||||||
|
shape = BitwardenTheme.shapes.bottomSheet,
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
BitwardenScaffold(
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = sheetTitle,
|
||||||
|
navigationIcon = NavigationIcon(
|
||||||
|
navigationIcon = rememberVectorPainter(R.drawable.ic_close),
|
||||||
|
onNavigationIconClick = onDismiss,
|
||||||
|
navigationIconContentDescription = stringResource(R.string.close),
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
minimunHeight = 64.dp,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.fillMaxSize(),
|
||||||
|
) { paddingValues ->
|
||||||
|
sheetContent(paddingValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.x8bit.bitwarden.ui.platform.components.content
|
package com.x8bit.bitwarden.ui.platform.components.content
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
@ -14,33 +15,33 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.ContentBlockData
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An overloaded version [BitwardenContentBlock] which takes a [String] for the header text.
|
* An overloaded version [BitwardenContentBlock] which takes a [ContentBlockData] for the
|
||||||
|
* header text.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BitwardenContentBlock(
|
fun BitwardenContentBlock(
|
||||||
headerText: String,
|
data: ContentBlockData,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
headerTextStyle: TextStyle = BitwardenTheme.typography.titleSmall,
|
headerTextStyle: TextStyle = BitwardenTheme.typography.titleSmall,
|
||||||
subtitleText: String? = null,
|
|
||||||
subtitleTextStyle: TextStyle = BitwardenTheme.typography.bodyMedium,
|
subtitleTextStyle: TextStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
iconPainter: Painter? = null,
|
|
||||||
backgroundColor: Color = BitwardenTheme.colorScheme.background.secondary,
|
backgroundColor: Color = BitwardenTheme.colorScheme.background.secondary,
|
||||||
) {
|
) {
|
||||||
BitwardenContentBlock(
|
BitwardenContentBlock(
|
||||||
headerText = AnnotatedString(headerText),
|
headerText = data.headerText,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
headerTextStyle = headerTextStyle,
|
headerTextStyle = headerTextStyle,
|
||||||
subtitleText = subtitleText,
|
subtitleText = data.subtitleText,
|
||||||
subtitleTextStyle = subtitleTextStyle,
|
subtitleTextStyle = subtitleTextStyle,
|
||||||
iconPainter = iconPainter,
|
iconVectorResource = data.iconVectorResource,
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -50,13 +51,13 @@ fun BitwardenContentBlock(
|
||||||
* Implemented to match design component.
|
* Implemented to match design component.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BitwardenContentBlock(
|
private fun BitwardenContentBlock(
|
||||||
headerText: AnnotatedString,
|
headerText: AnnotatedString,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
headerTextStyle: TextStyle = BitwardenTheme.typography.titleSmall,
|
headerTextStyle: TextStyle = BitwardenTheme.typography.titleSmall,
|
||||||
subtitleText: String? = null,
|
subtitleText: String? = null,
|
||||||
subtitleTextStyle: TextStyle = BitwardenTheme.typography.bodyMedium,
|
subtitleTextStyle: TextStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
iconPainter: Painter? = null,
|
@DrawableRes iconVectorResource: Int? = null,
|
||||||
backgroundColor: Color = BitwardenTheme.colorScheme.background.secondary,
|
backgroundColor: Color = BitwardenTheme.colorScheme.background.secondary,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
@ -65,15 +66,16 @@ fun BitwardenContentBlock(
|
||||||
.background(backgroundColor),
|
.background(backgroundColor),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
iconPainter
|
iconVectorResource
|
||||||
?.let {
|
?.let {
|
||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
Icon(
|
Icon(
|
||||||
painter = it,
|
painter = rememberVectorPainter(it),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = BitwardenTheme.colorScheme.icon.secondary,
|
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||||
modifier = Modifier.size(24.dp),
|
modifier = Modifier.size(24.dp),
|
||||||
)
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
}
|
}
|
||||||
?: Spacer(Modifier.width(16.dp))
|
?: Spacer(Modifier.width(16.dp))
|
||||||
|
|
||||||
|
@ -102,9 +104,11 @@ fun BitwardenContentBlock(
|
||||||
private fun BitwardenContentBlock_preview() {
|
private fun BitwardenContentBlock_preview() {
|
||||||
BitwardenTheme {
|
BitwardenTheme {
|
||||||
BitwardenContentBlock(
|
BitwardenContentBlock(
|
||||||
|
data = ContentBlockData(
|
||||||
headerText = "Header",
|
headerText = "Header",
|
||||||
subtitleText = "Subtitle",
|
subtitleText = "Subtitle",
|
||||||
iconPainter = null,
|
iconVectorResource = null,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components.model
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenContentBlock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper class for data to display in a
|
||||||
|
* [BitwardenContentBlock]
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class ContentBlockData(
|
||||||
|
val headerText: AnnotatedString,
|
||||||
|
val subtitleText: String? = null,
|
||||||
|
@DrawableRes val iconVectorResource: Int? = null,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Overloaded constructor for [ContentBlockData] that takes a [String] for the
|
||||||
|
* header text.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
headerText: String,
|
||||||
|
subtitleText: String? = null,
|
||||||
|
@DrawableRes iconVectorResource: Int? = null,
|
||||||
|
) : this(
|
||||||
|
headerText = AnnotatedString(headerText),
|
||||||
|
subtitleText = subtitleText,
|
||||||
|
iconVectorResource = iconVectorResource,
|
||||||
|
)
|
||||||
|
}
|
|
@ -221,10 +221,6 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
)
|
)
|
||||||
importLoginsScreenDestination(
|
importLoginsScreenDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToImportSuccessScreen = {
|
|
||||||
// TODO: PM-11187 navigate to success screen with popping this screen from stack
|
|
||||||
navController.popBackStack()
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,4 +18,5 @@ data class BitwardenShapes(
|
||||||
val menu: CornerBasedShape,
|
val menu: CornerBasedShape,
|
||||||
val segmentedControl: CornerBasedShape,
|
val segmentedControl: CornerBasedShape,
|
||||||
val snackbar: CornerBasedShape,
|
val snackbar: CornerBasedShape,
|
||||||
|
val bottomSheet: CornerBasedShape,
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,4 +18,5 @@ val bitwardenShapes: BitwardenShapes = BitwardenShapes(
|
||||||
menu = RoundedCornerShape(size = 4.dp),
|
menu = RoundedCornerShape(size = 4.dp),
|
||||||
segmentedControl = CircleShape,
|
segmentedControl = CircleShape,
|
||||||
snackbar = RoundedCornerShape(size = 8.dp),
|
snackbar = RoundedCornerShape(size = 8.dp),
|
||||||
|
bottomSheet = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp),
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,14 +19,12 @@ fun NavController.navigateToImportLoginsScreen(navOptions: NavOptions? = null) {
|
||||||
*/
|
*/
|
||||||
fun NavGraphBuilder.importLoginsScreenDestination(
|
fun NavGraphBuilder.importLoginsScreenDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToImportSuccessScreen: () -> Unit,
|
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = IMPORT_LOGINS_ROUTE,
|
route = IMPORT_LOGINS_ROUTE,
|
||||||
) {
|
) {
|
||||||
ImportLoginsScreen(
|
ImportLoginsScreen(
|
||||||
onNavigateBack = onNavigateBack,
|
onNavigateBack = onNavigateBack,
|
||||||
onNavigateToImportSuccessScreen = onNavigateToImportSuccessScreen,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,14 +13,17 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
@ -43,10 +46,14 @@ import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
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.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.bottomsheet.BitwardenModalBottomSheet
|
||||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
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.button.BitwardenOutlinedButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenContentBlock
|
||||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenFullScreenLoadingContent
|
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenFullScreenLoadingContent
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.ContentBlockData
|
||||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||||
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
|
||||||
|
@ -57,6 +64,7 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHan
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.rememberImportLoginHandler
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/"
|
private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/"
|
||||||
|
|
||||||
|
@ -68,7 +76,6 @@ private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/"
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportLoginsScreen(
|
fun ImportLoginsScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToImportSuccessScreen: () -> Unit,
|
|
||||||
viewModel: ImportLoginsViewModel = hiltViewModel(),
|
viewModel: ImportLoginsViewModel = hiltViewModel(),
|
||||||
intentManager: IntentManager = LocalIntentManager.current,
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
) {
|
) {
|
||||||
|
@ -81,8 +88,6 @@ fun ImportLoginsScreen(
|
||||||
ImportLoginsEvent.OpenHelpLink -> {
|
ImportLoginsEvent.OpenHelpLink -> {
|
||||||
intentManager.startCustomTabsActivity(IMPORT_HELP_URL.toUri())
|
intentManager.startCustomTabsActivity(IMPORT_HELP_URL.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportLoginsEvent.NavigateToImportSuccess -> onNavigateToImportSuccessScreen()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,6 +99,31 @@ fun ImportLoginsScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val hideSheetAndExecuteCompleteImportLogins: () -> Unit = {
|
||||||
|
// This pattern mirrors the onDismissRequest handling in the material ModalBottomSheet
|
||||||
|
scope
|
||||||
|
.launch {
|
||||||
|
sheetState.hide()
|
||||||
|
}
|
||||||
|
.invokeOnCompletion {
|
||||||
|
handler.onSuccessfulSyncAcknowledged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BitwardenModalBottomSheet(
|
||||||
|
showBottomSheet = state.showBottomSheet,
|
||||||
|
sheetTitle = stringResource(R.string.bitwarden_tools),
|
||||||
|
onDismiss = hideSheetAndExecuteCompleteImportLogins,
|
||||||
|
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||||
|
modifier = Modifier.statusBarsPadding(),
|
||||||
|
) { paddingValues ->
|
||||||
|
ImportLoginsSuccessBottomSheetContent(
|
||||||
|
onCompleteImportLogins = hideSheetAndExecuteCompleteImportLogins,
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
BitwardenFullScreenLoadingContent(
|
BitwardenFullScreenLoadingContent(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
@ -441,6 +471,85 @@ private fun ImportLoginsStepThreeContent(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun ImportLoginsSuccessBottomSheetContent(
|
||||||
|
onCompleteImportLogins: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.then(modifier),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Image(
|
||||||
|
painter = rememberVectorPainter(R.drawable.img_secure_devices),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.standardHorizontalMargin()
|
||||||
|
.size(124.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.import_successful),
|
||||||
|
style = BitwardenTheme.typography.titleMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.manage_your_logins_from_anywhere_with_bitwarden_tools,
|
||||||
|
),
|
||||||
|
style = BitwardenTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
BitwardenContentCard(
|
||||||
|
contentItems = persistentListOf(
|
||||||
|
ContentBlockData(
|
||||||
|
headerText = stringResource(R.string.download_the_browser_extension),
|
||||||
|
subtitleText = stringResource(
|
||||||
|
R.string.go_to_bitwarden_com_download_to_integrate_bitwarden_into_browser,
|
||||||
|
),
|
||||||
|
iconVectorResource = R.drawable.ic_puzzle,
|
||||||
|
),
|
||||||
|
ContentBlockData(
|
||||||
|
headerText = stringResource(R.string.use_the_web_app),
|
||||||
|
subtitleText = stringResource(
|
||||||
|
R.string.log_in_at_bitwarden_com_to_easily_manage_your_account,
|
||||||
|
),
|
||||||
|
iconVectorResource = R.drawable.ic_desktop,
|
||||||
|
),
|
||||||
|
ContentBlockData(
|
||||||
|
headerText = stringResource(R.string.autofill_passwords),
|
||||||
|
subtitleText = stringResource(R.string.set_up_autofill_on_all_your_devices),
|
||||||
|
iconVectorResource = R.drawable.ic_shield,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
|
) { contentData ->
|
||||||
|
BitwardenContentBlock(
|
||||||
|
data = contentData,
|
||||||
|
subtitleTextStyle = BitwardenTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
BitwardenFilledButton(
|
||||||
|
label = stringResource(R.string.got_it),
|
||||||
|
onClick = onCompleteImportLogins,
|
||||||
|
modifier = Modifier
|
||||||
|
.standardHorizontalMargin()
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -488,6 +597,7 @@ private fun ImportLoginsScreenDialog_preview(
|
||||||
onMoveToSyncInProgress = {},
|
onMoveToSyncInProgress = {},
|
||||||
onRetrySync = {},
|
onRetrySync = {},
|
||||||
onFailedSyncAcknowledged = {},
|
onFailedSyncAcknowledged = {},
|
||||||
|
onSuccessfulSyncAcknowledged = {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
InitialImportLoginsContent(
|
InitialImportLoginsContent(
|
||||||
|
@ -507,11 +617,13 @@ private class ImportLoginsDialogContentPreviewProvider :
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ class ImportLoginsViewModel @Inject constructor(
|
||||||
null,
|
null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
override fun handleAction(action: ImportLoginsAction) {
|
override fun handleAction(action: ImportLoginsAction) {
|
||||||
|
@ -46,10 +47,21 @@ class ImportLoginsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportLoginsAction.RetryVaultSync -> handleRetryVaultSync()
|
ImportLoginsAction.RetryVaultSync -> handleRetryVaultSync()
|
||||||
ImportLoginsAction.FailSyncAcknowledged -> handleFailedSyncAcknowledged()
|
ImportLoginsAction.FailedSyncAcknowledged -> handleFailedSyncAcknowledged()
|
||||||
|
ImportLoginsAction.SuccessfulSyncAcknowledged -> handleSuccessSyncAcknowledged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleSuccessSyncAcknowledged() {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
sendEvent(ImportLoginsEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleFailedSyncAcknowledged() {
|
private fun handleFailedSyncAcknowledged() {
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
it.copy(dialogState = null)
|
it.copy(dialogState = null)
|
||||||
|
@ -80,7 +92,14 @@ class ImportLoginsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncVaultDataResult.Success -> sendEvent(ImportLoginsEvent.NavigateToImportSuccess)
|
SyncVaultDataResult.Success -> {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
showBottomSheet = true,
|
||||||
|
isVaultSyncing = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,6 +185,7 @@ data class ImportLoginsState(
|
||||||
val dialogState: DialogState?,
|
val dialogState: DialogState?,
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
val isVaultSyncing: Boolean,
|
val isVaultSyncing: Boolean,
|
||||||
|
val showBottomSheet: Boolean,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Dialog states for the [ImportLoginsViewModel].
|
* Dialog states for the [ImportLoginsViewModel].
|
||||||
|
@ -249,11 +269,6 @@ sealed class ImportLoginsEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : ImportLoginsEvent()
|
data object NavigateBack : ImportLoginsEvent()
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate to the import success screen
|
|
||||||
*/
|
|
||||||
data object NavigateToImportSuccess : ImportLoginsEvent()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the help link in a browser.
|
* Open the help link in a browser.
|
||||||
*/
|
*/
|
||||||
|
@ -334,7 +349,12 @@ sealed class ImportLoginsAction {
|
||||||
/**
|
/**
|
||||||
* User has acknowledge failed sync and chose not to retry now.
|
* User has acknowledge failed sync and chose not to retry now.
|
||||||
*/
|
*/
|
||||||
data object FailSyncAcknowledged : ImportLoginsAction()
|
data object FailedSyncAcknowledged : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has imported logins successfully.
|
||||||
|
*/
|
||||||
|
data object SuccessfulSyncAcknowledged : ImportLoginsAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal actions to be handled, not triggered by user actions.
|
* Internal actions to be handled, not triggered by user actions.
|
||||||
|
|
|
@ -13,6 +13,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
|
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
|
||||||
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenContentBlock
|
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenContentBlock
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.model.ContentBlockData
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
@ -26,10 +27,13 @@ fun InstructionRowItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BitwardenContentBlock(
|
BitwardenContentBlock(
|
||||||
modifier = modifier,
|
data = ContentBlockData(
|
||||||
|
iconVectorResource = instructionStep.imageRes,
|
||||||
headerText = instructionStep.instructionText,
|
headerText = instructionStep.instructionText,
|
||||||
headerTextStyle = BitwardenTheme.typography.bodyMedium,
|
|
||||||
subtitleText = instructionStep.additionalText,
|
subtitleText = instructionStep.additionalText,
|
||||||
|
),
|
||||||
|
modifier = modifier,
|
||||||
|
headerTextStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
subtitleTextStyle = BitwardenTheme.typography.labelSmall,
|
subtitleTextStyle = BitwardenTheme.typography.labelSmall,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ data class ImportLoginHandler(
|
||||||
val onMoveToSyncInProgress: () -> Unit,
|
val onMoveToSyncInProgress: () -> Unit,
|
||||||
val onRetrySync: () -> Unit,
|
val onRetrySync: () -> Unit,
|
||||||
val onFailedSyncAcknowledged: () -> Unit,
|
val onFailedSyncAcknowledged: () -> Unit,
|
||||||
|
val onSuccessfulSyncAcknowledged: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@Suppress("UndocumentedPublicClass")
|
@Suppress("UndocumentedPublicClass")
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -50,7 +51,10 @@ data class ImportLoginHandler(
|
||||||
},
|
},
|
||||||
onRetrySync = { viewModel.trySendAction(ImportLoginsAction.RetryVaultSync) },
|
onRetrySync = { viewModel.trySendAction(ImportLoginsAction.RetryVaultSync) },
|
||||||
onFailedSyncAcknowledged = {
|
onFailedSyncAcknowledged = {
|
||||||
viewModel.trySendAction(ImportLoginsAction.FailSyncAcknowledged)
|
viewModel.trySendAction(ImportLoginsAction.FailedSyncAcknowledged)
|
||||||
|
},
|
||||||
|
onSuccessfulSyncAcknowledged = {
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.SuccessfulSyncAcknowledged)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
73
app/src/main/res/drawable-night/img_secure_devices.xml
Normal file
73
app/src/main/res/drawable-night/img_secure_devices.xml
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="200dp"
|
||||||
|
android:height="200dp"
|
||||||
|
android:viewportWidth="200"
|
||||||
|
android:viewportHeight="200">
|
||||||
|
<path
|
||||||
|
android:pathData="M18.75,38.17C18.75,31.26 24.35,25.67 31.25,25.67H168.75C175.65,25.67 181.25,31.26 181.25,38.17V125.67C181.25,132.57 175.65,138.17 168.75,138.17H31.25C24.35,138.17 18.75,132.57 18.75,125.67V38.17Z"
|
||||||
|
android:fillColor="#AAC3EF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M168.75,29.83H31.25C26.65,29.83 22.92,33.56 22.92,38.17V125.67C22.92,130.27 26.65,134 31.25,134H168.75C173.35,134 177.08,130.27 177.08,125.67V38.17C177.08,33.56 173.35,29.83 168.75,29.83ZM31.25,25.67C24.35,25.67 18.75,31.26 18.75,38.17V125.67C18.75,132.57 24.35,138.17 31.25,138.17H168.75C175.65,138.17 181.25,132.57 181.25,125.67V38.17C181.25,31.26 175.65,25.67 168.75,25.67H31.25Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.75,38.17H181.25V125.67H18.75V38.17Z"
|
||||||
|
android:fillColor="#79A1E9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M177.08,42.33H22.92V121.5H177.08V42.33ZM18.75,38.17V125.67H181.25V38.17H18.75Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M81.25,136.08H118.75V173.58H81.25V136.08Z"
|
||||||
|
android:fillColor="#AAC3EF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M79.17,134H120.83V171.5H137.5C138.65,171.5 139.58,172.43 139.58,173.58C139.58,174.73 138.65,175.67 137.5,175.67H62.5C61.35,175.67 60.42,174.73 60.42,173.58C60.42,172.43 61.35,171.5 62.5,171.5H79.17V134ZM83.33,171.5H116.67V138.17H83.33V171.5Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,100.67C0,96.06 3.73,92.33 8.33,92.33H45.83C50.44,92.33 54.17,96.06 54.17,100.67V167.33C54.17,171.93 50.44,175.67 45.83,175.67H8.33C3.73,175.67 0,171.93 0,167.33V100.67Z"
|
||||||
|
android:fillColor="#F3F6F9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M45.83,96.5H8.33C6.03,96.5 4.17,98.37 4.17,100.67V167.33C4.17,169.63 6.03,171.5 8.33,171.5H45.83C48.13,171.5 50,169.63 50,167.33V100.67C50,98.37 48.13,96.5 45.83,96.5ZM8.33,92.33C3.73,92.33 0,96.06 0,100.67V167.33C0,171.93 3.73,175.67 8.33,175.67H45.83C50.44,175.67 54.17,171.93 54.17,167.33V100.67C54.17,96.06 50.44,92.33 45.83,92.33H8.33Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.83,102.75C20.83,101.6 21.77,100.67 22.92,100.67L31.25,100.67C32.4,100.67 33.33,101.6 33.33,102.75C33.33,103.9 32.4,104.83 31.25,104.83L22.92,104.83C21.77,104.83 20.83,103.9 20.83,102.75Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M100,115.25C118.41,115.25 133.33,100.33 133.33,81.92C133.33,63.51 118.41,48.58 100,48.58C81.59,48.58 66.67,63.51 66.67,81.92C66.67,100.33 81.59,115.25 100,115.25ZM85.42,69.42C81.96,69.42 79.17,72.21 79.17,75.67V96.5C79.17,99.95 81.96,102.75 85.42,102.75H114.58C118.03,102.75 120.83,99.95 120.83,96.5V75.67C120.83,72.21 118.03,69.42 114.58,69.42H85.42Z"
|
||||||
|
android:fillColor="#AAC3EF"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M79.17,75.67C79.17,72.21 81.96,69.42 85.42,69.42H114.58C118.03,69.42 120.83,72.21 120.83,75.67V96.5C120.83,99.95 118.03,102.75 114.58,102.75H85.42C81.96,102.75 79.17,99.95 79.17,96.5V75.67Z"
|
||||||
|
android:fillColor="#FFBF00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M114.58,73.58H85.42C84.27,73.58 83.33,74.52 83.33,75.67V96.5C83.33,97.65 84.27,98.58 85.42,98.58H114.58C115.73,98.58 116.67,97.65 116.67,96.5V75.67C116.67,74.52 115.73,73.58 114.58,73.58ZM85.42,69.42C81.96,69.42 79.17,72.21 79.17,75.67V96.5C79.17,99.95 81.96,102.75 85.42,102.75H114.58C118.03,102.75 120.83,99.95 120.83,96.5V75.67C120.83,72.21 118.03,69.42 114.58,69.42H85.42Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M97.92,81.92C97.92,80.77 98.85,79.83 100,79.83C101.15,79.83 102.08,80.77 102.08,81.92V90.25C102.08,91.4 101.15,92.33 100,92.33C98.85,92.33 97.92,91.4 97.92,90.25V81.92Z"
|
||||||
|
android:fillColor="#175DDC"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M89.58,67.33C89.58,61.58 94.25,56.92 100,56.92C105.75,56.92 110.42,61.58 110.42,67.33V69.42H106.25V67.33C106.25,63.88 103.45,61.08 100,61.08C96.55,61.08 93.75,63.88 93.75,67.33V69.42H89.58V67.33Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M125,84C125,79.4 128.73,75.67 133.33,75.67H191.67C196.27,75.67 200,79.4 200,84V167.33C200,171.94 196.27,175.67 191.67,175.67H133.33C128.73,175.67 125,171.94 125,167.33V84Z"
|
||||||
|
android:fillColor="#F3F6F9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M191.67,79.83H133.33C131.03,79.83 129.17,81.7 129.17,84V167.33C129.17,169.63 131.03,171.5 133.33,171.5H191.67C193.97,171.5 195.83,169.63 195.83,167.33V84C195.83,81.7 193.97,79.83 191.67,79.83ZM133.33,75.67C128.73,75.67 125,79.4 125,84V167.33C125,171.94 128.73,175.67 133.33,175.67H191.67C196.27,175.67 200,171.94 200,167.33V84C200,79.4 196.27,75.67 191.67,75.67H133.33Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M168.75,159C168.75,162.45 165.95,165.25 162.5,165.25C159.05,165.25 156.25,162.45 156.25,159C156.25,155.55 159.05,152.75 162.5,152.75C165.95,152.75 168.75,155.55 168.75,159Z"
|
||||||
|
android:fillColor="#79A1E9"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M162.5,161.08C163.65,161.08 164.58,160.15 164.58,159C164.58,157.85 163.65,156.92 162.5,156.92C161.35,156.92 160.42,157.85 160.42,159C160.42,160.15 161.35,161.08 162.5,161.08ZM162.5,165.25C165.95,165.25 168.75,162.45 168.75,159C168.75,155.55 165.95,152.75 162.5,152.75C159.05,152.75 156.25,155.55 156.25,159C156.25,162.45 159.05,165.25 162.5,165.25Z"
|
||||||
|
android:fillColor="#175DDC"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M83.33,138.17H116.67V150.67H83.33V138.17Z"
|
||||||
|
android:fillColor="#79A1E9"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_desktop.xml
Normal file
10
app/src/main/res/drawable/ic_desktop.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M3.2,3.333C1.985,3.333 1,4.34 1,5.583V15.089C1,16.331 1.985,17.338 3.2,17.338H8.769V19.646H7.531C7.076,19.646 6.706,20.024 6.706,20.489C6.706,20.955 7.076,21.333 7.531,21.333H16.469C16.924,21.333 17.294,20.955 17.294,20.489C17.294,20.024 16.924,19.646 16.469,19.646H15.231V17.338H20.8C22.015,17.338 23,16.331 23,15.089V5.583C23,4.34 22.015,3.333 20.8,3.333H3.2ZM20.8,5.02H3.2C2.896,5.02 2.65,5.272 2.65,5.583V15.089C2.65,15.399 2.896,15.651 3.2,15.651H20.8C21.104,15.651 21.35,15.399 21.35,15.089V5.583C21.35,5.272 21.104,5.02 20.8,5.02ZM13.581,19.646V17.338H10.419V19.646L13.581,19.646Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
10
app/src/main/res/drawable/ic_puzzle.xml
Normal file
10
app/src/main/res/drawable/ic_puzzle.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M1.324,5.934C1.324,4.802 2.242,3.884 3.374,3.884H16.375C17.507,3.884 18.425,4.802 18.425,5.934V8.908C18.822,8.642 19.289,8.479 19.8,8.479C20.676,8.479 21.412,8.952 21.904,9.606C22.396,10.26 22.678,11.129 22.678,12.057C22.678,12.985 22.396,13.854 21.904,14.508C21.412,15.162 20.676,15.635 19.8,15.635C19.289,15.635 18.822,15.472 18.425,15.206V18.181C18.425,19.313 17.507,20.231 16.375,20.231H12.08C11.427,20.231 10.897,19.701 10.897,19.048V17.494C10.897,17.066 11.124,16.715 11.411,16.506C11.797,16.226 11.962,15.893 11.962,15.592C11.962,15.296 11.803,14.97 11.432,14.693C11.06,14.415 10.512,14.221 9.875,14.221C9.238,14.221 8.69,14.415 8.319,14.693C7.947,14.97 7.789,15.296 7.789,15.592C7.789,15.892 7.953,16.225 8.338,16.505C8.624,16.714 8.851,17.065 8.851,17.492V19.048C8.851,19.701 8.321,20.231 7.668,20.231H3.374C2.242,20.231 1.324,19.313 1.324,18.181V5.934ZM3.374,5.384C3.07,5.384 2.824,5.63 2.824,5.934V18.181C2.824,18.485 3.07,18.731 3.374,18.731H7.351V17.639C6.732,17.149 6.289,16.437 6.289,15.592C6.289,14.716 6.765,13.981 7.42,13.491C8.076,13.001 8.946,12.721 9.875,12.721C10.805,12.721 11.675,13.001 12.33,13.491C12.985,13.981 13.462,14.716 13.462,15.592C13.462,16.438 13.018,17.151 12.397,17.641V18.731H16.375C16.678,18.731 16.925,18.485 16.925,18.181V14.612C16.925,13.958 17.454,13.428 18.108,13.428H18.193C18.55,13.428 18.852,13.588 19.057,13.793C19.297,14.03 19.554,14.135 19.8,14.135C20.1,14.135 20.427,13.976 20.705,13.606C20.983,13.236 21.178,12.691 21.178,12.057C21.178,11.423 20.983,10.878 20.705,10.508C20.427,10.139 20.1,9.979 19.8,9.979C19.554,9.979 19.296,10.084 19.057,10.322C18.851,10.526 18.55,10.686 18.193,10.686H18.108C17.454,10.686 16.925,10.156 16.925,9.503V5.934C16.925,5.63 16.678,5.384 16.375,5.384H3.374Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
13
app/src/main/res/drawable/ic_shield.xml
Normal file
13
app/src/main/res/drawable/ic_shield.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h20v20h-20z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.186,0.583C18.109,0.504 18.017,0.441 17.916,0.397C17.814,0.354 17.704,0.332 17.593,0.333H2.408C2.297,0.332 2.187,0.354 2.085,0.397C1.983,0.441 1.892,0.504 1.816,0.583C1.735,0.658 1.671,0.749 1.628,0.85C1.584,0.951 1.562,1.059 1.563,1.168V11.168C1.566,11.929 1.715,12.683 2.003,13.389C2.277,14.086 2.645,14.743 3.098,15.343C3.563,15.943 4.083,16.499 4.653,17.004C5.181,17.483 5.738,17.931 6.319,18.345C6.826,18.7 7.357,19.037 7.914,19.354C8.471,19.672 8.864,19.886 9.093,19.998C9.325,20.11 9.512,20.199 9.651,20.258C9.76,20.31 9.88,20.335 10.001,20.333C10.12,20.335 10.237,20.307 10.344,20.254C10.485,20.193 10.67,20.107 10.904,19.994C11.138,19.882 11.537,19.667 12.084,19.35C12.63,19.034 13.168,18.697 13.678,18.342C14.26,17.927 14.818,17.479 15.347,17C15.917,16.496 16.438,15.94 16.903,15.339C17.354,14.739 17.722,14.082 17.997,13.385C18.286,12.679 18.435,11.925 18.438,11.164V1.164C18.438,1.056 18.416,0.948 18.372,0.848C18.329,0.748 18.265,0.658 18.186,0.583ZM16.228,11.259C16.228,14.884 10.001,17.998 10.001,17.998V2.476H16.228V11.259Z"
|
||||||
|
android:fillColor="#000000"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
77
app/src/main/res/drawable/img_secure_devices.xml
Normal file
77
app/src/main/res/drawable/img_secure_devices.xml
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="200dp"
|
||||||
|
android:height="200dp"
|
||||||
|
android:viewportWidth="200"
|
||||||
|
android:viewportHeight="200">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.67h200v200h-200z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.75,38.17C18.75,31.26 24.35,25.67 31.25,25.67H168.75C175.65,25.67 181.25,31.26 181.25,38.17V125.67C181.25,132.57 175.65,138.17 168.75,138.17H31.25C24.35,138.17 18.75,132.57 18.75,125.67V38.17Z"
|
||||||
|
android:fillColor="#DBE5F6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M168.75,29.83H31.25C26.65,29.83 22.92,33.56 22.92,38.17V125.67C22.92,130.27 26.65,134 31.25,134H168.75C173.35,134 177.08,130.27 177.08,125.67V38.17C177.08,33.56 173.35,29.83 168.75,29.83ZM31.25,25.67C24.35,25.67 18.75,31.26 18.75,38.17V125.67C18.75,132.57 24.35,138.17 31.25,138.17H168.75C175.65,138.17 181.25,132.57 181.25,125.67V38.17C181.25,31.26 175.65,25.67 168.75,25.67H31.25Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M18.75,38.17H181.25V125.67H18.75V38.17Z"
|
||||||
|
android:fillColor="#AAC3EF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M177.08,42.33H22.92V121.5H177.08V42.33ZM18.75,38.17V125.67H181.25V38.17H18.75Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M81.25,136.08H118.75V173.58H81.25V136.08Z"
|
||||||
|
android:fillColor="#DBE5F6"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M79.17,134H120.83V171.5H137.5C138.65,171.5 139.58,172.43 139.58,173.58C139.58,174.73 138.65,175.67 137.5,175.67H62.5C61.35,175.67 60.42,174.73 60.42,173.58C60.42,172.43 61.35,171.5 62.5,171.5H79.17V134ZM83.33,171.5H116.67V138.17H83.33V171.5Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M0,100.67C0,96.06 3.73,92.33 8.33,92.33H45.83C50.44,92.33 54.17,96.06 54.17,100.67V167.33C54.17,171.93 50.44,175.67 45.83,175.67H8.33C3.73,175.67 0,171.93 0,167.33V100.67Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M45.83,96.5H8.33C6.03,96.5 4.17,98.37 4.17,100.67V167.33C4.17,169.63 6.03,171.5 8.33,171.5H45.83C48.13,171.5 50,169.63 50,167.33V100.67C50,98.37 48.13,96.5 45.83,96.5ZM8.33,92.33C3.73,92.33 0,96.06 0,100.67V167.33C0,171.93 3.73,175.67 8.33,175.67H45.83C50.44,175.67 54.17,171.93 54.17,167.33V100.67C54.17,96.06 50.44,92.33 45.83,92.33H8.33Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M20.83,102.75C20.83,101.6 21.77,100.67 22.92,100.67L31.25,100.67C32.4,100.67 33.33,101.6 33.33,102.75C33.33,103.9 32.4,104.83 31.25,104.83L22.92,104.83C21.77,104.83 20.83,103.9 20.83,102.75Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M100,115.25C118.41,115.25 133.33,100.33 133.33,81.92C133.33,63.51 118.41,48.58 100,48.58C81.59,48.58 66.67,63.51 66.67,81.92C66.67,100.33 81.59,115.25 100,115.25ZM85.42,69.42C81.96,69.42 79.17,72.21 79.17,75.67V96.5C79.17,99.95 81.96,102.75 85.42,102.75H114.58C118.03,102.75 120.83,99.95 120.83,96.5V75.67C120.83,72.21 118.03,69.42 114.58,69.42H85.42Z"
|
||||||
|
android:fillColor="#DBE5F6"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M79.17,75.67C79.17,72.21 81.96,69.42 85.42,69.42H114.58C118.03,69.42 120.83,72.21 120.83,75.67V96.5C120.83,99.95 118.03,102.75 114.58,102.75H85.42C81.96,102.75 79.17,99.95 79.17,96.5V75.67Z"
|
||||||
|
android:fillColor="#FFBF00"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M114.58,73.58H85.42C84.27,73.58 83.33,74.52 83.33,75.67V96.5C83.33,97.65 84.27,98.58 85.42,98.58H114.58C115.73,98.58 116.67,97.65 116.67,96.5V75.67C116.67,74.52 115.73,73.58 114.58,73.58ZM85.42,69.42C81.96,69.42 79.17,72.21 79.17,75.67V96.5C79.17,99.95 81.96,102.75 85.42,102.75H114.58C118.03,102.75 120.83,99.95 120.83,96.5V75.67C120.83,72.21 118.03,69.42 114.58,69.42H85.42Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M97.92,81.92C97.92,80.77 98.85,79.83 100,79.83C101.15,79.83 102.08,80.77 102.08,81.92V90.25C102.08,91.4 101.15,92.33 100,92.33C98.85,92.33 97.92,91.4 97.92,90.25V81.92Z"
|
||||||
|
android:fillColor="#020F66"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M89.58,67.33C89.58,61.58 94.25,56.92 100,56.92C105.75,56.92 110.42,61.58 110.42,67.33V69.42H106.25V67.33C106.25,63.88 103.45,61.08 100,61.08C96.55,61.08 93.75,63.88 93.75,67.33V69.42H89.58V67.33Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M125,84C125,79.4 128.73,75.67 133.33,75.67H191.67C196.27,75.67 200,79.4 200,84V167.33C200,171.94 196.27,175.67 191.67,175.67H133.33C128.73,175.67 125,171.94 125,167.33V84Z"
|
||||||
|
android:fillColor="#ffffff"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M191.67,79.83H133.33C131.03,79.83 129.17,81.7 129.17,84V167.33C129.17,169.63 131.03,171.5 133.33,171.5H191.67C193.97,171.5 195.83,169.63 195.83,167.33V84C195.83,81.7 193.97,79.83 191.67,79.83ZM133.33,75.67C128.73,75.67 125,79.4 125,84V167.33C125,171.94 128.73,175.67 133.33,175.67H191.67C196.27,175.67 200,171.94 200,167.33V84C200,79.4 196.27,75.67 191.67,75.67H133.33Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M168.75,159C168.75,162.45 165.95,165.25 162.5,165.25C159.05,165.25 156.25,162.45 156.25,159C156.25,155.55 159.05,152.75 162.5,152.75C165.95,152.75 168.75,155.55 168.75,159Z"
|
||||||
|
android:fillColor="#AAC3EF"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M162.5,161.08C163.65,161.08 164.58,160.15 164.58,159C164.58,157.85 163.65,156.92 162.5,156.92C161.35,156.92 160.42,157.85 160.42,159C160.42,160.15 161.35,161.08 162.5,161.08ZM162.5,165.25C165.95,165.25 168.75,162.45 168.75,159C168.75,155.55 165.95,152.75 162.5,152.75C159.05,152.75 156.25,155.55 156.25,159C156.25,162.45 159.05,165.25 162.5,165.25Z"
|
||||||
|
android:fillColor="#020F66"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M83.33,138.17H116.67V150.67H83.33V138.17Z"
|
||||||
|
android:fillColor="#AAC3EF"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -1062,4 +1062,14 @@ Do you want to switch to this account?</string>
|
||||||
<string name="this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server">This is not a recognized Bitwarden server. You may need to check with your provider or update your server.</string>
|
<string name="this_is_not_a_recognized_bitwarden_server_you_may_need_to_check_with_your_provider_or_update_your_server">This is not a recognized Bitwarden server. You may need to check with your provider or update your server.</string>
|
||||||
<string name="syncing_logins_loading_message">Syncing logins...</string>
|
<string name="syncing_logins_loading_message">Syncing logins...</string>
|
||||||
<string name="ssh_key_cipher_item_types">SSH Key Cipher Item Types</string>
|
<string name="ssh_key_cipher_item_types">SSH Key Cipher Item Types</string>
|
||||||
|
<string name="download_the_browser_extension">Download the browser extension</string>
|
||||||
|
<string name="go_to_bitwarden_com_download_to_integrate_bitwarden_into_browser">Go to bitwarden.com/download to integrate Bitwarden into your favorite browser for a seamless experience.</string>
|
||||||
|
<string name="use_the_web_app">Use the web app</string>
|
||||||
|
<string name="log_in_at_bitwarden_com_to_easily_manage_your_account">Log in at bitwarden.com to easily manage your account and update settings.</string>
|
||||||
|
<string name="autofill_passwords">Autofill passwords</string>
|
||||||
|
<string name="set_up_autofill_on_all_your_devices">Set up autofill on all your devices to login with a single tap anywhere.</string>
|
||||||
|
<string name="import_successful">Import Successful!</string>
|
||||||
|
<string name="manage_your_logins_from_anywhere_with_bitwarden_tools">Manage your logins from anywhere with Bitwarden tools for web and desktop.</string>
|
||||||
|
<string name="bitwarden_tools">Bitwarden Tools</string>
|
||||||
|
<string name="got_it">Got it</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -4,9 +4,12 @@ import androidx.activity.OnBackPressedDispatcher
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.test.ExperimentalTestApi
|
||||||
import androidx.compose.ui.test.junit4.createComposeRule
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
import com.x8bit.bitwarden.ui.platform.feature.settings.appearance.model.AppTheme
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,8 +17,12 @@ import org.junit.Rule
|
||||||
* Testing, and JUnit 4.
|
* Testing, and JUnit 4.
|
||||||
*/
|
*/
|
||||||
abstract class BaseComposeTest : BaseRobolectricTest() {
|
abstract class BaseComposeTest : BaseRobolectricTest() {
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
protected val dispatcher = UnconfinedTestDispatcher()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTestApi::class)
|
||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule(effectContext = dispatcher)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* instance of [OnBackPressedDispatcher] made available if testing using
|
* instance of [OnBackPressedDispatcher] made available if testing using
|
||||||
|
|
|
@ -1,18 +1,24 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||||
|
|
||||||
|
import androidx.compose.ui.semantics.SemanticsActions
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
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.hasAnySibling
|
||||||
|
import androidx.compose.ui.test.hasText
|
||||||
import androidx.compose.ui.test.isDialog
|
import androidx.compose.ui.test.isDialog
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithContentDescription
|
||||||
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.onNodeWithText
|
||||||
import androidx.compose.ui.test.onRoot
|
import androidx.compose.ui.test.onRoot
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performScrollTo
|
import androidx.compose.ui.test.performScrollTo
|
||||||
|
import androidx.compose.ui.test.performSemanticsAction
|
||||||
import androidx.compose.ui.test.printToLog
|
import androidx.compose.ui.test.printToLog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||||
|
@ -28,7 +34,6 @@ import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class ImportLoginsScreenTest : BaseComposeTest() {
|
class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
private var navigateToImportLoginSuccessCalled = false
|
|
||||||
private var navigateBackCalled = false
|
private var navigateBackCalled = false
|
||||||
|
|
||||||
private val mutableImportLoginsStateFlow = MutableStateFlow(DEFAULT_STATE)
|
private val mutableImportLoginsStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
@ -47,7 +52,6 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
setContentWithBackDispatcher {
|
setContentWithBackDispatcher {
|
||||||
ImportLoginsScreen(
|
ImportLoginsScreen(
|
||||||
onNavigateBack = { navigateBackCalled = true },
|
onNavigateBack = { navigateBackCalled = true },
|
||||||
onNavigateToImportSuccessScreen = { navigateToImportLoginSuccessCalled = true },
|
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
intentManager = intentManager,
|
intentManager = intentManager,
|
||||||
)
|
)
|
||||||
|
@ -90,28 +94,28 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `dialog content is shown when state updates and is hidden when null`() {
|
fun `dialog content is shown when state updates and is hidden when null`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNode(isDialog())
|
.onNode(isDialog())
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.assertNoDialogExists()
|
.assertNoDialogExists()
|
||||||
|
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNode(isDialog())
|
.onNode(isDialog())
|
||||||
|
@ -121,11 +125,11 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `when dialog state is GetStarted, GetStarted dialog is shown and sends correct actions when clicked`() {
|
fun `when dialog state is GetStarted, GetStarted dialog is shown and sends correct actions when clicked`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNode(isDialog())
|
.onNode(isDialog())
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -153,11 +157,11 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `when dialog state is ImportLater, ImportLater dialog is shown and sends correct actions when clicked`() {
|
fun `when dialog state is ImportLater, ImportLater dialog is shown and sends correct actions when clicked`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNode(isDialog())
|
.onNode(isDialog())
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -191,22 +195,22 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on initial content system back sends CloseClick action`() {
|
fun `while on initial content system back sends CloseClick action`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
backDispatcher?.onBackPressed()
|
backDispatcher?.onBackPressed()
|
||||||
verifyActionSent(ImportLoginsAction.CloseClick)
|
verifyActionSent(ImportLoginsAction.CloseClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Step one content is displayed when view state is ImportStepOne`() {
|
fun `Step one content is displayed when view state is ImportStepOne`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Step 1 of 3")
|
.onNodeWithText("Step 1 of 3")
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -214,11 +218,11 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on step one correct actions are sent when buttons are clicked`() {
|
fun `while on step one correct actions are sent when buttons are clicked`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Back")
|
.onNodeWithText("Back")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -232,22 +236,22 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on step one system back returns to the previous content`() {
|
fun `while on step one system back returns to the previous content`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
backDispatcher?.onBackPressed()
|
backDispatcher?.onBackPressed()
|
||||||
verifyActionSent(ImportLoginsAction.MoveToInitialContent)
|
verifyActionSent(ImportLoginsAction.MoveToInitialContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Step two content is displayed when view state is ImportStepTwo`() {
|
fun `Step two content is displayed when view state is ImportStepTwo`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Step 2 of 3")
|
.onNodeWithText("Step 2 of 3")
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -255,11 +259,11 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on step two correct actions are sent when buttons are clicked`() {
|
fun `while on step two correct actions are sent when buttons are clicked`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Back")
|
.onNodeWithText("Back")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -273,22 +277,22 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on step two system back returns to the previous content`() {
|
fun `while on step two system back returns to the previous content`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
backDispatcher?.onBackPressed()
|
backDispatcher?.onBackPressed()
|
||||||
verifyActionSent(ImportLoginsAction.MoveToStepOne)
|
verifyActionSent(ImportLoginsAction.MoveToStepOne)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Step three content is displayed when view state is ImportStepThree`() {
|
fun `Step three content is displayed when view state is ImportStepThree`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Step 3 of 3")
|
.onNodeWithText("Step 3 of 3")
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -296,11 +300,11 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on step three correct actions are sent when buttons are clicked`() {
|
fun `while on step three correct actions are sent when buttons are clicked`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNodeWithText("Back")
|
.onNodeWithText("Back")
|
||||||
.performScrollTo()
|
.performScrollTo()
|
||||||
|
@ -314,21 +318,15 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `while on step three system back returns to the previous content`() {
|
fun `while on step three system back returns to the previous content`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
backDispatcher?.onBackPressed()
|
backDispatcher?.onBackPressed()
|
||||||
verifyActionSent(ImportLoginsAction.MoveToStepTwo)
|
verifyActionSent(ImportLoginsAction.MoveToStepTwo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `NavigateToImportSuccess event causes correct lambda to invoke`() {
|
|
||||||
mutableImportLoginsEventFlow.tryEmit(ImportLoginsEvent.NavigateToImportSuccess)
|
|
||||||
assertTrue(navigateToImportLoginSuccessCalled)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Loading content is displayed when isVaultSyncing is true`() {
|
fun `Loading content is displayed when isVaultSyncing is true`() {
|
||||||
mutableImportLoginsStateFlow.update {
|
mutableImportLoginsStateFlow.update {
|
||||||
|
@ -342,11 +340,11 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Error dialog is displayed when dialog state is Error`() {
|
fun `Error dialog is displayed when dialog state is Error`() {
|
||||||
mutableImportLoginsStateFlow.tryEmit(
|
mutableImportLoginsStateFlow.update {
|
||||||
DEFAULT_STATE.copy(
|
it.copy(
|
||||||
dialogState = ImportLoginsState.DialogState.Error,
|
dialogState = ImportLoginsState.DialogState.Error,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
composeTestRule
|
composeTestRule
|
||||||
.onNode(isDialog())
|
.onNode(isDialog())
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
|
@ -370,7 +368,57 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
.filterToOne(hasAnyAncestor(isDialog()))
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
.assertIsDisplayed()
|
.assertIsDisplayed()
|
||||||
.performClick()
|
.performClick()
|
||||||
verifyActionSent(ImportLoginsAction.FailSyncAcknowledged)
|
verifyActionSent(ImportLoginsAction.FailedSyncAcknowledged)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Success bottom sheet is shown when state is updated`() {
|
||||||
|
mutableImportLoginsStateFlow.update {
|
||||||
|
it.copy(showBottomSheet = true)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Import Successful!")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SuccessfulSyncAcknowledged action is sent when bottom sheet CTA is clicked`() {
|
||||||
|
mutableImportLoginsStateFlow.update {
|
||||||
|
it.copy(showBottomSheet = true)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Import Successful!")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Got it")
|
||||||
|
.performScrollTo()
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performSemanticsAction(SemanticsActions.OnClick)
|
||||||
|
|
||||||
|
dispatcher.advanceTimeByAndRunCurrent(1000L)
|
||||||
|
|
||||||
|
verifyActionSent(ImportLoginsAction.SuccessfulSyncAcknowledged)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SuccessfulSyncAcknowledged action is sent when bottom sheet is closed`() {
|
||||||
|
mutableImportLoginsStateFlow.update {
|
||||||
|
it.copy(showBottomSheet = true)
|
||||||
|
}
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Import Successful!")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithContentDescription("Close")
|
||||||
|
.filterToOne(hasAnySibling(hasText("Bitwarden Tools")))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
.performSemanticsAction(SemanticsActions.OnClick)
|
||||||
|
|
||||||
|
dispatcher.advanceTimeByAndRunCurrent(1000L)
|
||||||
|
|
||||||
|
verifyActionSent(ImportLoginsAction.SuccessfulSyncAcknowledged)
|
||||||
}
|
}
|
||||||
|
|
||||||
//region Helper methods
|
//region Helper methods
|
||||||
|
@ -386,4 +434,5 @@ private val DEFAULT_STATE = ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -38,6 +38,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -52,6 +53,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -71,6 +73,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -80,6 +83,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -102,6 +106,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
stateFlow.awaitItem(),
|
stateFlow.awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -111,6 +116,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
stateFlow.awaitItem(),
|
stateFlow.awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -135,6 +141,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -144,6 +151,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -183,6 +191,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -197,6 +206,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -211,6 +221,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -229,23 +240,32 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `MoveToSyncInProgress sets isVaultSyncing to true and calls syncForResult`() {
|
fun `MoveToSyncInProgress sets isVaultSyncing to true and calls syncForResult`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(
|
||||||
|
DEFAULT_STATE,
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = true,
|
isVaultSyncing = true,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
coVerify { vaultRepository.syncForResult() }
|
coVerify { vaultRepository.syncForResult() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,21 +285,28 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = true,
|
isVaultSyncing = true,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
coVerify { vaultRepository.syncForResult() }
|
coVerify { vaultRepository.syncForResult() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `MoveToSyncInProgress should send NavigateToImportSuccess event when sync succeeds`() =
|
fun `SyncVaultDataResult success should update state to show bottom sheet`() {
|
||||||
runTest {
|
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.eventFlow.test {
|
|
||||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
assertEquals(ImportLoginsEvent.NavigateToImportSuccess, awaitItem())
|
assertEquals(
|
||||||
}
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = true,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -292,13 +319,14 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
dialogState = ImportLoginsState.DialogState.Error,
|
dialogState = ImportLoginsState.DialogState.Error,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on FailSyncAcknowledged should remove dialog state and send NavigateBack event`() =
|
fun `FailSyncAcknowledged should remove dialog state and send NavigateBack event`() =
|
||||||
runTest {
|
runTest {
|
||||||
coEvery {
|
coEvery {
|
||||||
vaultRepository.syncForResult()
|
vaultRepository.syncForResult()
|
||||||
|
@ -307,12 +335,13 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
viewModel.eventFlow.test {
|
viewModel.eventFlow.test {
|
||||||
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
assertNotNull(viewModel.stateFlow.value.dialogState)
|
assertNotNull(viewModel.stateFlow.value.dialogState)
|
||||||
viewModel.trySendAction(ImportLoginsAction.FailSyncAcknowledged)
|
viewModel.trySendAction(ImportLoginsAction.FailedSyncAcknowledged)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -323,6 +352,45 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SuccessfulSyncAcknowledged should hide bottom sheet and send NavigateBack`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateEventFlow(backgroundScope = backgroundScope) { stateFlow, eventFlow ->
|
||||||
|
// Initial state
|
||||||
|
assertEquals(DEFAULT_STATE, stateFlow.awaitItem())
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
isVaultSyncing = true,
|
||||||
|
showBottomSheet = false,
|
||||||
|
),
|
||||||
|
stateFlow.awaitItem(),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = true,
|
||||||
|
),
|
||||||
|
stateFlow.awaitItem(),
|
||||||
|
)
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.SuccessfulSyncAcknowledged)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
|
),
|
||||||
|
stateFlow.awaitItem(),
|
||||||
|
)
|
||||||
|
assertEquals(ImportLoginsEvent.NavigateBack, eventFlow.awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel(
|
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel(
|
||||||
vaultRepository = vaultRepository,
|
vaultRepository = vaultRepository,
|
||||||
)
|
)
|
||||||
|
@ -332,4 +400,5 @@ private val DEFAULT_STATE = ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
viewState = ImportLoginsState.ViewState.InitialContent,
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
isVaultSyncing = false,
|
isVaultSyncing = false,
|
||||||
|
showBottomSheet = false,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue