mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 07:05:35 +03:00
PM-11182 PM-11183 PM-11184 Add the instruction steps to logins import flow (#4089)
This commit is contained in:
parent
c382227b6a
commit
ab9d57b4f2
16 changed files with 1183 additions and 41 deletions
|
@ -6,6 +6,7 @@ import androidx.annotation.PluralsRes
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.LinkAnnotation
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
@ -135,20 +136,13 @@ fun @receiver:StringRes Int.asText(vararg args: Any): Text = ResArgsText(this, a
|
||||||
fun createAnnotatedString(
|
fun createAnnotatedString(
|
||||||
mainString: String,
|
mainString: String,
|
||||||
highlights: List<String>,
|
highlights: List<String>,
|
||||||
highlightStyle: SpanStyle = SpanStyle(
|
highlightStyle: SpanStyle = bitwardenClickableTextSpanStyle,
|
||||||
color = BitwardenTheme.colorScheme.text.interaction,
|
tag: String? = null,
|
||||||
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
),
|
|
||||||
tag: String,
|
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
return buildAnnotatedString {
|
return buildAnnotatedString {
|
||||||
append(mainString)
|
append(mainString)
|
||||||
addStyle(
|
addStyle(
|
||||||
style = SpanStyle(
|
style = bitwardenDefaultSpanStyle,
|
||||||
color = BitwardenTheme.colorScheme.text.primary,
|
|
||||||
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
|
||||||
),
|
|
||||||
start = 0,
|
start = 0,
|
||||||
end = mainString.length,
|
end = mainString.length,
|
||||||
)
|
)
|
||||||
|
@ -160,8 +154,9 @@ fun createAnnotatedString(
|
||||||
start = startIndex,
|
start = startIndex,
|
||||||
end = endIndex,
|
end = endIndex,
|
||||||
)
|
)
|
||||||
|
tag?.let {
|
||||||
addStringAnnotation(
|
addStringAnnotation(
|
||||||
tag = tag,
|
tag = it,
|
||||||
annotation = highlightString,
|
annotation = highlightString,
|
||||||
start = startIndex,
|
start = startIndex,
|
||||||
end = endIndex,
|
end = endIndex,
|
||||||
|
@ -169,6 +164,7 @@ fun createAnnotatedString(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an [AnnotatedString] with highlighted parts that can be clicked.
|
* Create an [AnnotatedString] with highlighted parts that can be clicked.
|
||||||
|
@ -182,15 +178,8 @@ fun createAnnotatedString(
|
||||||
fun createClickableAnnotatedString(
|
fun createClickableAnnotatedString(
|
||||||
mainString: String,
|
mainString: String,
|
||||||
highlights: List<ClickableTextHighlight>,
|
highlights: List<ClickableTextHighlight>,
|
||||||
style: SpanStyle = SpanStyle(
|
style: SpanStyle = bitwardenDefaultSpanStyle,
|
||||||
color = BitwardenTheme.colorScheme.text.primary,
|
highlightStyle: SpanStyle = bitwardenClickableTextSpanStyle,
|
||||||
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
|
||||||
),
|
|
||||||
highlightStyle: SpanStyle = SpanStyle(
|
|
||||||
color = BitwardenTheme.colorScheme.text.interaction,
|
|
||||||
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
),
|
|
||||||
): AnnotatedString {
|
): AnnotatedString {
|
||||||
return buildAnnotatedString {
|
return buildAnnotatedString {
|
||||||
append(mainString)
|
append(mainString)
|
||||||
|
@ -250,3 +239,26 @@ data class ClickableTextHighlight(
|
||||||
LAST,
|
LAST,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bitwardenDefaultSpanStyle: SpanStyle
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = SpanStyle(
|
||||||
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
fontSize = BitwardenTheme.typography.bodyMedium.fontSize,
|
||||||
|
fontFamily = BitwardenTheme.typography.bodyMedium.fontFamily,
|
||||||
|
)
|
||||||
|
|
||||||
|
val bitwardenBoldSpanStyle: SpanStyle
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = bitwardenDefaultSpanStyle.copy(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
|
||||||
|
val bitwardenClickableTextSpanStyle: SpanStyle
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = bitwardenBoldSpanStyle.copy(
|
||||||
|
color = BitwardenTheme.colorScheme.text.interaction,
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components.card
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.bottomDivider
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable card for displaying content for a list of items with a generic type [T].
|
||||||
|
* Items will be displayed in [Column] in the order they are provided with an optional divider
|
||||||
|
* below them, besides the last item in the list.
|
||||||
|
*
|
||||||
|
* @param contentItems list of items to display.
|
||||||
|
* @param content composable to render each item to the UI.
|
||||||
|
* @param showBottomDivider whether to show a divider below each item.
|
||||||
|
* @param bottomDividerPaddingStart padding to apply to the start of the divider.
|
||||||
|
* @param bottomDividerPaddingEnd padding to apply to the end of the divider.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun <T> BitwardenContentCard(
|
||||||
|
contentItems: ImmutableList<T>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showBottomDivider: Boolean = true,
|
||||||
|
bottomDividerPaddingStart: Dp = 0.dp,
|
||||||
|
bottomDividerPaddingEnd: Dp = 0.dp,
|
||||||
|
content: @Composable (T) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
color = BitwardenTheme.colorScheme.background.secondary,
|
||||||
|
shape = BitwardenTheme.shapes.content,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
contentItems.forEachIndexed { index, item ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.bottomDivider(
|
||||||
|
enabled = index != contentItems.lastIndex && showBottomDivider,
|
||||||
|
paddingStart = bottomDividerPaddingStart,
|
||||||
|
paddingEnd = bottomDividerPaddingEnd,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
content(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components.content
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An overloaded version [BitwardenContentBlock] which takes a [String] for the header text.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenContentBlock(
|
||||||
|
headerText: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
headerTextStyle: TextStyle = BitwardenTheme.typography.titleSmall,
|
||||||
|
subtitleText: String? = null,
|
||||||
|
subtitleTextStyle: TextStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
|
iconPainter: Painter? = null,
|
||||||
|
backgroundColor: Color = BitwardenTheme.colorScheme.background.secondary,
|
||||||
|
) {
|
||||||
|
BitwardenContentBlock(
|
||||||
|
headerText = AnnotatedString(headerText),
|
||||||
|
modifier = modifier,
|
||||||
|
headerTextStyle = headerTextStyle,
|
||||||
|
subtitleText = subtitleText,
|
||||||
|
subtitleTextStyle = subtitleTextStyle,
|
||||||
|
iconPainter = iconPainter,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A default content block which displays a header with an optional subtitle and an icon.
|
||||||
|
* Implemented to match design component.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenContentBlock(
|
||||||
|
headerText: AnnotatedString,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
headerTextStyle: TextStyle = BitwardenTheme.typography.titleSmall,
|
||||||
|
subtitleText: String? = null,
|
||||||
|
subtitleTextStyle: TextStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
|
iconPainter: Painter? = null,
|
||||||
|
backgroundColor: Color = BitwardenTheme.colorScheme.background.secondary,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(backgroundColor),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
iconPainter
|
||||||
|
?.let {
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Icon(
|
||||||
|
painter = it,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
?: Spacer(Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Text(
|
||||||
|
text = headerText,
|
||||||
|
style = headerTextStyle,
|
||||||
|
)
|
||||||
|
subtitleText?.let {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = subtitleTextStyle,
|
||||||
|
color = BitwardenTheme.colorScheme.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun BitwardenContentBlock_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
BitwardenContentBlock(
|
||||||
|
headerText = "Header",
|
||||||
|
subtitleText = "Subtitle",
|
||||||
|
iconPainter = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
package com.x8bit.bitwarden.ui.vault.feature.importlogins
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
@ -23,16 +25,21 @@ 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
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
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.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.bitwardenBoldSpanStyle
|
||||||
|
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
|
||||||
|
@ -41,18 +48,27 @@ 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.dialog.BitwardenTwoButtonDialog
|
||||||
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.manager.intent.IntentManager
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.components.ImportLoginsInstructionStep
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHandler
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.handlers.ImportLoginHandler
|
||||||
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 kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
private const val IMPORT_HELP_URL = "https://bitwarden.com/help/import-data/"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Top level component for the import logins screen.
|
* Top level component for the import logins screen.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ImportLoginsScreen(
|
fun ImportLoginsScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
viewModel: ImportLoginsViewModel = hiltViewModel(),
|
viewModel: ImportLoginsViewModel = hiltViewModel(),
|
||||||
|
intentManager: IntentManager = LocalIntentManager.current,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val handler = rememberImportLoginHandler(viewModel = viewModel)
|
val handler = rememberImportLoginHandler(viewModel = viewModel)
|
||||||
|
@ -60,11 +76,20 @@ fun ImportLoginsScreen(
|
||||||
EventsEffect(viewModel = viewModel) { event ->
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
ImportLoginsEvent.NavigateBack -> onNavigateBack()
|
ImportLoginsEvent.NavigateBack -> onNavigateBack()
|
||||||
|
ImportLoginsEvent.OpenHelpLink -> {
|
||||||
|
intentManager.startCustomTabsActivity(IMPORT_HELP_URL.toUri())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportLoginsDialogContent(state = state, handler = handler)
|
ImportLoginsDialogContent(state = state, handler = handler)
|
||||||
|
|
||||||
|
BackHandler(enabled = true) {
|
||||||
|
state.viewState.backAction?.let {
|
||||||
|
viewModel.trySendAction(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
@ -82,16 +107,50 @@ fun ImportLoginsScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
Column(
|
Crossfade(
|
||||||
|
targetState = state.viewState,
|
||||||
|
label = "CrossfadeBetweenViewStates",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues = innerPadding),
|
.padding(paddingValues = innerPadding),
|
||||||
) {
|
) { viewState ->
|
||||||
ImportLoginsContent(
|
when (viewState) {
|
||||||
|
ImportLoginsState.ViewState.InitialContent -> {
|
||||||
|
InitialImportLoginsContent(
|
||||||
onGetStartedClick = handler.onGetStartedClick,
|
onGetStartedClick = handler.onGetStartedClick,
|
||||||
onImportLaterClick = handler.onImportLaterClick,
|
onImportLaterClick = handler.onImportLaterClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImportLoginsState.ViewState.ImportStepOne -> {
|
||||||
|
ImportLoginsStepOneContent(
|
||||||
|
onBackClick = handler.onMoveToInitialContent,
|
||||||
|
onContinueClick = handler.onMoveToStepTwo,
|
||||||
|
onHelpClick = handler.onHelpClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportLoginsState.ViewState.ImportStepTwo -> {
|
||||||
|
ImportLoginsStepTwoContent(
|
||||||
|
onBackClick = handler.onMoveToStepOne,
|
||||||
|
onContinueClick = handler.onMoveToStepThree,
|
||||||
|
onHelpClick = handler.onHelpClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportLoginsState.ViewState.ImportStepThree -> {
|
||||||
|
ImportLoginsStepThreeContent(
|
||||||
|
onBackClick = handler.onMoveToStepTwo,
|
||||||
|
onContinueClick = handler.onMoveToSyncInProgress,
|
||||||
|
onHelpClick = handler.onHelpClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportLoginsState.ViewState.SyncInProgress -> {
|
||||||
|
// TODO PM-11186: Implement sync in progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +191,7 @@ private fun ImportLoginsDialogContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ImportLoginsContent(
|
private fun InitialImportLoginsContent(
|
||||||
onGetStartedClick: () -> Unit,
|
onGetStartedClick: () -> Unit,
|
||||||
onImportLaterClick: () -> Unit,
|
onImportLaterClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
@ -189,6 +248,181 @@ private fun ImportLoginsContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ImportLoginsStepOneContent(
|
||||||
|
onContinueClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onHelpClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val instruction1 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.on_your_computer_log_in_to_your_current_browser_or_password_manager,
|
||||||
|
),
|
||||||
|
highlights = listOf(
|
||||||
|
stringResource(R.string.log_in_to_your_current_browser_or_password_manager_highlight),
|
||||||
|
),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
val instruction2 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.export_your_passwords_this_option_is_usually_found_in_your_settings,
|
||||||
|
),
|
||||||
|
listOf(stringResource(R.string.export_your_passwords_highlight)),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
val instruction3 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.save_the_exported_file_somewhere_on_your_computer_you_can_find_easily,
|
||||||
|
),
|
||||||
|
highlights = listOf(stringResource(R.string.save_the_exported_file_highlight)),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
ImportLoginsInstructionStep(
|
||||||
|
stepText = stringResource(R.string.step_1_of_3),
|
||||||
|
stepTitle = stringResource(R.string.export_your_saved_logins),
|
||||||
|
instructions = persistentListOf(
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 1,
|
||||||
|
instructionText = instruction1,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 2,
|
||||||
|
instructionText = instruction2,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 3,
|
||||||
|
instructionText = instruction3,
|
||||||
|
additionalText = stringResource(R.string.delete_this_file_after_import_is_complete),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onContinueClick = onContinueClick,
|
||||||
|
onHelpClick = onHelpClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ImportLoginsStepTwoContent(
|
||||||
|
onContinueClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onHelpClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val instruction1 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.on_your_computer_open_a_new_browser_tab_and_go_to_vault_bitwarden_com,
|
||||||
|
),
|
||||||
|
highlights = listOf(stringResource(R.string.go_to_vault_bitwarden_com_highlight)),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
val instruction2Text = stringResource(R.string.log_in_to_the_bitwarden_web_app)
|
||||||
|
val instruction2 = buildAnnotatedString {
|
||||||
|
withStyle(bitwardenBoldSpanStyle) {
|
||||||
|
append(instruction2Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImportLoginsInstructionStep(
|
||||||
|
stepText = stringResource(R.string.step_2_of_3),
|
||||||
|
stepTitle = stringResource(R.string.log_in_to_bitwarden),
|
||||||
|
instructions = persistentListOf(
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 1,
|
||||||
|
instructionText = instruction1,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 2,
|
||||||
|
instructionText = instruction2,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onContinueClick = onContinueClick,
|
||||||
|
onHelpClick = onHelpClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
private fun ImportLoginsStepThreeContent(
|
||||||
|
onContinueClick: () -> Unit,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onHelpClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val instruction1 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.in_the_bitwarden_navigation_find_the_tools_option_and_select_import_data,
|
||||||
|
),
|
||||||
|
highlights = listOf(
|
||||||
|
stringResource(R.string.find_the_tools_highlight),
|
||||||
|
stringResource(R.string.select_import_data_step_3_highlight),
|
||||||
|
),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
val instruction2 = createAnnotatedString(
|
||||||
|
mainString = stringResource(R.string.fill_out_the_form_and_import_your_saved_password_file),
|
||||||
|
highlights = listOf(
|
||||||
|
stringResource(R.string.import_your_saved_password_file_highlight),
|
||||||
|
),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
val instruction3 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.select_import_data_in_the_web_app_then_done_to_finish_syncing,
|
||||||
|
),
|
||||||
|
highlights = listOf(
|
||||||
|
stringResource(R.string.select_import_data_highlight),
|
||||||
|
stringResource(R.string.then_done_highlight),
|
||||||
|
),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
val instruction4 = createAnnotatedString(
|
||||||
|
mainString = stringResource(
|
||||||
|
R.string.for_your_security_be_sure_to_delete_your_saved_password_file,
|
||||||
|
),
|
||||||
|
highlights = listOf(
|
||||||
|
stringResource(R.string.delete_your_saved_password_file),
|
||||||
|
),
|
||||||
|
highlightStyle = bitwardenBoldSpanStyle,
|
||||||
|
)
|
||||||
|
ImportLoginsInstructionStep(
|
||||||
|
stepText = stringResource(R.string.step_3_of_3),
|
||||||
|
stepTitle = stringResource(R.string.import_logins_to_bitwarden),
|
||||||
|
instructions = persistentListOf(
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 1,
|
||||||
|
instructionText = instruction1,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 2,
|
||||||
|
instructionText = instruction2,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 3,
|
||||||
|
instructionText = instruction3,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 4,
|
||||||
|
instructionText = instruction4,
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onContinueClick = onContinueClick,
|
||||||
|
onHelpClick = onHelpClick,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -199,7 +433,7 @@ private fun ImportLoginsInitialContent_preview() {
|
||||||
BitwardenTheme.colorScheme.background.primary,
|
BitwardenTheme.colorScheme.background.primary,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
ImportLoginsContent(
|
InitialImportLoginsContent(
|
||||||
onGetStartedClick = {},
|
onGetStartedClick = {},
|
||||||
onImportLaterClick = {},
|
onImportLaterClick = {},
|
||||||
)
|
)
|
||||||
|
@ -228,9 +462,15 @@ private fun ImportLoginsScreenDialog_preview(
|
||||||
onCloseClick = {},
|
onCloseClick = {},
|
||||||
onGetStartedClick = {},
|
onGetStartedClick = {},
|
||||||
onImportLaterClick = {},
|
onImportLaterClick = {},
|
||||||
|
onHelpClick = {},
|
||||||
|
onMoveToInitialContent = {},
|
||||||
|
onMoveToStepOne = {},
|
||||||
|
onMoveToStepTwo = {},
|
||||||
|
onMoveToStepThree = {},
|
||||||
|
onMoveToSyncInProgress = {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ImportLoginsContent(
|
InitialImportLoginsContent(
|
||||||
onGetStartedClick = {},
|
onGetStartedClick = {},
|
||||||
onImportLaterClick = {},
|
onImportLaterClick = {},
|
||||||
)
|
)
|
||||||
|
@ -245,9 +485,11 @@ private class ImportLoginsDialogContentPreviewProvider :
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,13 @@ import javax.inject.Inject
|
||||||
/**
|
/**
|
||||||
* View model for the [ImportLoginsScreen].
|
* View model for the [ImportLoginsScreen].
|
||||||
*/
|
*/
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ImportLoginsViewModel @Inject constructor() :
|
class ImportLoginsViewModel @Inject constructor() :
|
||||||
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
|
BaseViewModel<ImportLoginsState, ImportLoginsEvent, ImportLoginsAction>(
|
||||||
initialState = ImportLoginsState(
|
initialState = ImportLoginsState(
|
||||||
null,
|
null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
override fun handleAction(action: ImportLoginsAction) {
|
override fun handleAction(action: ImportLoginsAction) {
|
||||||
|
@ -26,9 +28,39 @@ class ImportLoginsViewModel @Inject constructor() :
|
||||||
ImportLoginsAction.GetStartedClick -> handleGetStartedClick()
|
ImportLoginsAction.GetStartedClick -> handleGetStartedClick()
|
||||||
ImportLoginsAction.ImportLaterClick -> handleImportLaterClick()
|
ImportLoginsAction.ImportLaterClick -> handleImportLaterClick()
|
||||||
ImportLoginsAction.CloseClick -> handleCloseClick()
|
ImportLoginsAction.CloseClick -> handleCloseClick()
|
||||||
|
ImportLoginsAction.MoveToInitialContent -> handleMoveToInitialContent()
|
||||||
|
ImportLoginsAction.MoveToStepOne -> handleMoveToStepOne()
|
||||||
|
ImportLoginsAction.MoveToStepTwo -> handleMoveToStepTwo()
|
||||||
|
ImportLoginsAction.MoveToStepThree -> handleMoveToStepThree()
|
||||||
|
ImportLoginsAction.MoveToSyncInProgress -> handleMoveToSyncInProgress()
|
||||||
|
ImportLoginsAction.HelpClick -> handleHelpClick()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleMoveToSyncInProgress() {
|
||||||
|
// TODO PM-11186: Implement sync in progress
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleHelpClick() {
|
||||||
|
sendEvent(ImportLoginsEvent.OpenHelpLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMoveToStepThree() {
|
||||||
|
updateViewState(ImportLoginsState.ViewState.ImportStepThree)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMoveToStepTwo() {
|
||||||
|
updateViewState(ImportLoginsState.ViewState.ImportStepTwo)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMoveToStepOne() {
|
||||||
|
updateViewState(ImportLoginsState.ViewState.ImportStepOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMoveToInitialContent() {
|
||||||
|
updateViewState(ImportLoginsState.ViewState.InitialContent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCloseClick() {
|
private fun handleCloseClick() {
|
||||||
sendEvent(ImportLoginsEvent.NavigateBack)
|
sendEvent(ImportLoginsEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
|
@ -52,7 +84,13 @@ class ImportLoginsViewModel @Inject constructor() :
|
||||||
|
|
||||||
private fun handleConfirmGetStarted() {
|
private fun handleConfirmGetStarted() {
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
// TODO - PM-11182: Move to first step in instructions.
|
updateViewState(ImportLoginsState.ViewState.ImportStepOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateViewState(viewState: ImportLoginsState.ViewState) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(viewState = viewState)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dismissDialog() {
|
private fun dismissDialog() {
|
||||||
|
@ -71,6 +109,7 @@ class ImportLoginsViewModel @Inject constructor() :
|
||||||
*/
|
*/
|
||||||
data class ImportLoginsState(
|
data class ImportLoginsState(
|
||||||
val dialogState: DialogState?,
|
val dialogState: DialogState?,
|
||||||
|
val viewState: ViewState,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Dialog states for the [ImportLoginsViewModel].
|
* Dialog states for the [ImportLoginsViewModel].
|
||||||
|
@ -97,6 +136,51 @@ data class ImportLoginsState(
|
||||||
override val title: Text = R.string.do_you_have_a_computer_available.asText()
|
override val title: Text = R.string.do_you_have_a_computer_available.asText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View states for the [ImportLoginsViewModel].
|
||||||
|
*/
|
||||||
|
sealed class ViewState {
|
||||||
|
/**
|
||||||
|
* Back action for each view state.
|
||||||
|
*/
|
||||||
|
abstract val backAction: ImportLoginsAction?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial content view state.
|
||||||
|
*/
|
||||||
|
data object InitialContent : ViewState() {
|
||||||
|
override val backAction: ImportLoginsAction = ImportLoginsAction.CloseClick
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import step one view state.
|
||||||
|
*/
|
||||||
|
data object ImportStepOne : ViewState() {
|
||||||
|
override val backAction: ImportLoginsAction = ImportLoginsAction.MoveToInitialContent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import step two view state.
|
||||||
|
*/
|
||||||
|
data object ImportStepTwo : ViewState() {
|
||||||
|
override val backAction: ImportLoginsAction = ImportLoginsAction.MoveToStepOne
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import step three view state.
|
||||||
|
*/
|
||||||
|
data object ImportStepThree : ViewState() {
|
||||||
|
override val backAction: ImportLoginsAction = ImportLoginsAction.MoveToStepTwo
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync in progress view state.
|
||||||
|
*/
|
||||||
|
data object SyncInProgress : ViewState() {
|
||||||
|
override val backAction: ImportLoginsAction? = null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,6 +191,11 @@ sealed class ImportLoginsEvent {
|
||||||
* Navigate back to the previous screen.
|
* Navigate back to the previous screen.
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : ImportLoginsEvent()
|
data object NavigateBack : ImportLoginsEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the help link in a browser.
|
||||||
|
*/
|
||||||
|
data object OpenHelpLink : ImportLoginsEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,4 +232,34 @@ sealed class ImportLoginsAction {
|
||||||
* User has clicked the "Close" icon button.
|
* User has clicked the "Close" icon button.
|
||||||
*/
|
*/
|
||||||
data object CloseClick : ImportLoginsAction()
|
data object CloseClick : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the "Help" button.
|
||||||
|
*/
|
||||||
|
data object HelpClick : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has performed action which should move to the initial content view state.
|
||||||
|
*/
|
||||||
|
data object MoveToInitialContent : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has performed action which should move to the step one view state.
|
||||||
|
*/
|
||||||
|
data object MoveToStepOne : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has performed action which should move to the step two view state.
|
||||||
|
*/
|
||||||
|
data object MoveToStepTwo : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has performed action which should move to the step three view state.
|
||||||
|
*/
|
||||||
|
data object MoveToStepThree : ImportLoginsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has performed action which should move to the sync in progress view state.
|
||||||
|
*/
|
||||||
|
data object MoveToSyncInProgress : ImportLoginsAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,153 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.importlogins.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.ClickableTextHighlight
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.createClickableAnnotatedString
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||||
|
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.card.BitwardenContentCard
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable component for each step of the import logins flow.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
fun ImportLoginsInstructionStep(
|
||||||
|
stepText: String,
|
||||||
|
stepTitle: String,
|
||||||
|
instructions: ImmutableList<InstructionStep>,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onContinueClick: () -> Unit,
|
||||||
|
onHelpClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = stepText,
|
||||||
|
style = BitwardenTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
Text(text = stepTitle, style = BitwardenTheme.typography.titleMedium)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
BitwardenContentCard(
|
||||||
|
contentItems = instructions,
|
||||||
|
bottomDividerPaddingStart = 48.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
) { instructionStep ->
|
||||||
|
InstructionRowItem(
|
||||||
|
instructionStep = instructionStep,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(all = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
Text(
|
||||||
|
text = createClickableAnnotatedString(
|
||||||
|
mainString = stringResource(R.string.need_help_checkout_out_import_help),
|
||||||
|
highlights = listOf(
|
||||||
|
ClickableTextHighlight(
|
||||||
|
textToHighlight = stringResource(R.string.import_help_highlight),
|
||||||
|
onTextClick = onHelpClick,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style = BitwardenTheme.typography.bodySmall,
|
||||||
|
color = BitwardenTheme.colorScheme.text.secondary,
|
||||||
|
modifier = Modifier.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(24.dp))
|
||||||
|
BitwardenFilledButton(
|
||||||
|
label = stringResource(R.string.continue_text),
|
||||||
|
onClick = onContinueClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
BitwardenOutlinedButton(
|
||||||
|
label = stringResource(R.string.back),
|
||||||
|
onClick = onBackClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.standardHorizontalMargin(),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ImportLoginsInstructionStep_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
Column(modifier = Modifier.background(BitwardenTheme.colorScheme.background.primary)) {
|
||||||
|
ImportLoginsInstructionStep(
|
||||||
|
stepText = "Step text",
|
||||||
|
stepTitle = "Step title",
|
||||||
|
instructions = persistentListOf(
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 1,
|
||||||
|
instructionText = buildAnnotatedString {
|
||||||
|
append("Step text 1")
|
||||||
|
withStyle(
|
||||||
|
SpanStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = BitwardenTheme.typography.bodyMedium.fontFamily,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
append(" with bold text")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 2,
|
||||||
|
instructionText = buildAnnotatedString {
|
||||||
|
append("Step text 2")
|
||||||
|
},
|
||||||
|
additionalText = "Added deets",
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 3,
|
||||||
|
instructionText = buildAnnotatedString {
|
||||||
|
append("Step text 3")
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onBackClick = {},
|
||||||
|
onContinueClick = {},
|
||||||
|
onHelpClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.importlogins.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenContentCard
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.content.BitwardenContentBlock
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.model.InstructionStep
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Row item for the content card of the import logins screen instructions.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun InstructionRowItem(
|
||||||
|
instructionStep: InstructionStep,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
BitwardenContentBlock(
|
||||||
|
modifier = modifier,
|
||||||
|
headerText = instructionStep.instructionText,
|
||||||
|
headerTextStyle = BitwardenTheme.typography.bodyMedium,
|
||||||
|
subtitleText = instructionStep.additionalText,
|
||||||
|
subtitleTextStyle = BitwardenTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
@get:DrawableRes
|
||||||
|
private val InstructionStep.imageRes: Int
|
||||||
|
get() = when (this.stepNumber) {
|
||||||
|
1 -> R.drawable.ic_number1
|
||||||
|
2 -> R.drawable.ic_number2
|
||||||
|
3 -> R.drawable.ic_number3
|
||||||
|
4 -> R.drawable.ic_number4
|
||||||
|
else -> error(
|
||||||
|
"Invalid step number, if new step is required please add drawable asset for it.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun InstructionCard_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
Surface {
|
||||||
|
BitwardenContentCard(
|
||||||
|
contentItems = persistentListOf(
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 1,
|
||||||
|
instructionText = buildAnnotatedString {
|
||||||
|
append("Step text 1")
|
||||||
|
withStyle(
|
||||||
|
SpanStyle(
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontFamily = BitwardenTheme.typography.bodyMedium.fontFamily,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
append(" with bold text")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
additionalText = null,
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 2,
|
||||||
|
instructionText = buildAnnotatedString {
|
||||||
|
append("Step text 2")
|
||||||
|
},
|
||||||
|
additionalText = "Added deets",
|
||||||
|
),
|
||||||
|
InstructionStep(
|
||||||
|
stepNumber = 3,
|
||||||
|
instructionText = buildAnnotatedString {
|
||||||
|
append("Step text 3")
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
InstructionRowItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsAction
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsViewModel
|
import com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action handlers for the [ImportLoginsScreen].
|
* Action handlers for the [com.x8bit.bitwarden.ui.vault.feature.importlogins.ImportLoginsScreen].
|
||||||
*/
|
*/
|
||||||
data class ImportLoginHandler(
|
data class ImportLoginHandler(
|
||||||
val onGetStartedClick: () -> Unit,
|
val onGetStartedClick: () -> Unit,
|
||||||
|
@ -15,6 +15,12 @@ data class ImportLoginHandler(
|
||||||
val onConfirmGetStarted: () -> Unit,
|
val onConfirmGetStarted: () -> Unit,
|
||||||
val onConfirmImportLater: () -> Unit,
|
val onConfirmImportLater: () -> Unit,
|
||||||
val onCloseClick: () -> Unit,
|
val onCloseClick: () -> Unit,
|
||||||
|
val onHelpClick: () -> Unit,
|
||||||
|
val onMoveToInitialContent: () -> Unit,
|
||||||
|
val onMoveToStepOne: () -> Unit,
|
||||||
|
val onMoveToStepTwo: () -> Unit,
|
||||||
|
val onMoveToStepThree: () -> Unit,
|
||||||
|
val onMoveToSyncInProgress: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@Suppress("UndocumentedPublicClass")
|
@Suppress("UndocumentedPublicClass")
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -30,6 +36,16 @@ data class ImportLoginHandler(
|
||||||
viewModel.trySendAction(ImportLoginsAction.ConfirmImportLater)
|
viewModel.trySendAction(ImportLoginsAction.ConfirmImportLater)
|
||||||
},
|
},
|
||||||
onCloseClick = { viewModel.trySendAction(ImportLoginsAction.CloseClick) },
|
onCloseClick = { viewModel.trySendAction(ImportLoginsAction.CloseClick) },
|
||||||
|
onHelpClick = { viewModel.trySendAction(ImportLoginsAction.HelpClick) },
|
||||||
|
onMoveToInitialContent = {
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToInitialContent)
|
||||||
|
},
|
||||||
|
onMoveToStepOne = { viewModel.trySendAction(ImportLoginsAction.MoveToStepOne) },
|
||||||
|
onMoveToStepTwo = { viewModel.trySendAction(ImportLoginsAction.MoveToStepTwo) },
|
||||||
|
onMoveToStepThree = { viewModel.trySendAction(ImportLoginsAction.MoveToStepThree) },
|
||||||
|
onMoveToSyncInProgress = {
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.importlogins.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models a single instruction step to be displayed in the import login instructions card.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class InstructionStep(
|
||||||
|
val stepNumber: Int,
|
||||||
|
val instructionText: AnnotatedString,
|
||||||
|
val additionalText: String? = null,
|
||||||
|
)
|
13
app/src/main/res/drawable/ic_number1.xml
Normal file
13
app/src/main/res/drawable/ic_number1.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="17dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="17">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M7.68,14.333V4.762L5.572,5.306V3.725L8.496,2.433H9.771V14.333H7.68Z"
|
||||||
|
android:fillColor="#175DDC"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
17
app/src/main/res/drawable/ic_number2.xml
Normal file
17
app/src/main/res/drawable/ic_number2.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="17dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="17">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M4.012,14.333V12.854C4.772,12.253 5.497,11.653 6.188,11.052C6.88,10.451 7.503,9.856 8.058,9.267C8.614,8.678 9.05,8.1 9.367,7.533C9.685,6.966 9.843,6.422 9.843,5.901C9.843,5.561 9.781,5.244 9.656,4.949C9.532,4.654 9.333,4.416 9.061,4.235C8.801,4.054 8.449,3.963 8.007,3.963C7.577,3.963 7.208,4.059 6.902,4.252C6.608,4.445 6.387,4.705 6.239,5.034C6.092,5.351 6.018,5.714 6.018,6.122H4.046C4.069,5.261 4.256,4.541 4.607,3.963C4.959,3.385 5.435,2.954 6.035,2.671C6.636,2.376 7.31,2.229 8.058,2.229C8.863,2.229 9.549,2.376 10.115,2.671C10.682,2.966 11.118,3.379 11.424,3.912C11.73,4.445 11.883,5.074 11.883,5.799C11.883,6.332 11.781,6.859 11.577,7.38C11.385,7.89 11.113,8.389 10.761,8.876C10.41,9.352 10.013,9.817 9.571,10.27C9.141,10.712 8.693,11.137 8.228,11.545C7.764,11.942 7.316,12.31 6.885,12.65H12.206V14.333H4.012Z"
|
||||||
|
android:fillColor="#175DDC"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
17
app/src/main/res/drawable/ic_number3.xml
Normal file
17
app/src/main/res/drawable/ic_number3.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="17dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="17">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M8.478,14.537C7.707,14.537 7.004,14.401 6.37,14.129C5.735,13.846 5.225,13.421 4.84,12.854C4.466,12.276 4.267,11.551 4.245,10.678H6.251C6.262,11.075 6.353,11.437 6.523,11.766C6.704,12.083 6.959,12.338 7.288,12.531C7.616,12.712 8.013,12.803 8.478,12.803C8.92,12.803 9.294,12.718 9.6,12.548C9.917,12.378 10.155,12.146 10.314,11.851C10.472,11.556 10.552,11.228 10.552,10.865C10.552,10.423 10.444,10.06 10.229,9.777C10.013,9.482 9.719,9.261 9.345,9.114C8.971,8.967 8.546,8.893 8.07,8.893H7.186V7.21H8.07C8.693,7.21 9.203,7.068 9.6,6.785C9.996,6.49 10.195,6.065 10.195,5.51C10.195,5.045 10.042,4.671 9.736,4.388C9.441,4.105 9.016,3.963 8.461,3.963C7.883,3.963 7.424,4.133 7.084,4.473C6.755,4.813 6.574,5.232 6.54,5.731H4.534C4.568,5.017 4.749,4.399 5.078,3.878C5.418,3.345 5.877,2.937 6.455,2.654C7.033,2.371 7.707,2.229 8.478,2.229C9.282,2.229 9.962,2.371 10.518,2.654C11.073,2.937 11.492,3.317 11.776,3.793C12.07,4.269 12.218,4.79 12.218,5.357C12.218,5.799 12.133,6.201 11.963,6.564C11.793,6.927 11.56,7.233 11.266,7.482C10.982,7.72 10.648,7.896 10.263,8.009C10.705,8.1 11.096,8.275 11.436,8.536C11.787,8.797 12.059,9.131 12.252,9.539C12.456,9.936 12.558,10.4 12.558,10.933C12.558,11.579 12.399,12.18 12.082,12.735C11.776,13.279 11.317,13.715 10.705,14.044C10.104,14.373 9.362,14.537 8.478,14.537Z"
|
||||||
|
android:fillColor="#175DDC"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
17
app/src/main/res/drawable/ic_number4.xml
Normal file
17
app/src/main/res/drawable/ic_number4.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="17dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="17">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M0,0.333h16v16h-16z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M9.71,14.333V12.072H3.811V10.44L9.421,2.433H11.733V10.287H13.348V12.072H11.733V14.333H9.71ZM6.004,10.287H9.829V4.677L6.004,10.287Z"
|
||||||
|
android:fillColor="#175DDC"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
|
@ -1030,4 +1030,32 @@ Do you want to switch to this account?</string>
|
||||||
<string name="give_your_vault_a_head_start">Give your vault a head start</string>
|
<string name="give_your_vault_a_head_start">Give your vault a head start</string>
|
||||||
<string name="class_3_biometrics_description">Unlock with biometrics requires strong biometric authentication and may not be compatible with all biometric options on this device.</string>
|
<string name="class_3_biometrics_description">Unlock with biometrics requires strong biometric authentication and may not be compatible with all biometric options on this device.</string>
|
||||||
<string name="class_2_biometrics_description">Unlock with biometrics requires strong biometric authentication and is not compatible with the biometrics options available on this device.</string>
|
<string name="class_2_biometrics_description">Unlock with biometrics requires strong biometric authentication and is not compatible with the biometrics options available on this device.</string>
|
||||||
|
<string name="on_your_computer_log_in_to_your_current_browser_or_password_manager">On your computer, log in to your current browser or password manager.</string>
|
||||||
|
<string name="log_in_to_your_current_browser_or_password_manager_highlight">log in to your current browser or password manager.</string>
|
||||||
|
<string name="export_your_passwords_this_option_is_usually_found_in_your_settings">Export your passwords. This option is usually found in your settings.</string>
|
||||||
|
<string name="export_your_passwords_highlight">Export your passwords.</string>
|
||||||
|
<string name="select_import_data_in_the_web_app_then_done_to_finish_syncing">Select Import data in the web app, then Done to finish syncing.</string>
|
||||||
|
<string name="select_import_data_highlight">Select Import data</string>
|
||||||
|
<string name="step_1_of_3">Step 1 of 3</string>
|
||||||
|
<string name="export_your_saved_logins">Export your saved logins</string>
|
||||||
|
<string name="delete_this_file_after_import_is_complete">You’ll delete this file after import is complete.</string>
|
||||||
|
<string name="on_your_computer_open_a_new_browser_tab_and_go_to_vault_bitwarden_com">On your computer, open a new browser tab and go to vault.bitwarden.com</string>
|
||||||
|
<string name="go_to_vault_bitwarden_com_highlight">go to vault.bitwarden.com</string>
|
||||||
|
<string name="log_in_to_the_bitwarden_web_app">Log in to the Bitwarden web app.</string>
|
||||||
|
<string name="step_2_of_3">Step 2 of 3</string>
|
||||||
|
<string name="log_in_to_bitwarden">Log in to Bitwarden</string>
|
||||||
|
<string name="step_3_of_3">Step 3 of 3</string>
|
||||||
|
<string name="import_logins_to_bitwarden">Import logins to Bitwarden</string>
|
||||||
|
<string name="in_the_bitwarden_navigation_find_the_tools_option_and_select_import_data">In the Bitwarden navigation, find the Tools option and select Import data.</string>
|
||||||
|
<string name="find_the_tools_highlight">find the Tools</string>
|
||||||
|
<string name="select_import_data_step_3_highlight">select Import data.</string>
|
||||||
|
<string name="fill_out_the_form_and_import_your_saved_password_file">Fill out the form and import your saved password file.</string>
|
||||||
|
<string name="import_your_saved_password_file_highlight">import your saved password file.</string>
|
||||||
|
<string name="then_done_highlight">then Done</string>
|
||||||
|
<string name="for_your_security_be_sure_to_delete_your_saved_password_file">For your security, be sure to delete your saved password file.</string>
|
||||||
|
<string name="delete_your_saved_password_file">delete your saved password file.</string>
|
||||||
|
<string name="need_help_checkout_out_import_help">Need help? Checkout out import help.</string>
|
||||||
|
<string name="import_help_highlight">import help</string>
|
||||||
|
<string name="save_the_exported_file_somewhere_on_your_computer_you_can_find_easily">Save the exported file somewhere on your computer you can find easily.</string>
|
||||||
|
<string name="save_the_exported_file_highlight">Save the exported file</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -8,8 +8,11 @@ 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.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performScrollTo
|
||||||
|
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.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.util.assertNoDialogExists
|
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -32,13 +35,16 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
every { stateFlow } returns mutableImportLoginsStateFlow
|
every { stateFlow } returns mutableImportLoginsStateFlow
|
||||||
every { trySendAction(any()) } just runs
|
every { trySendAction(any()) } just runs
|
||||||
}
|
}
|
||||||
|
private val intentManager = mockk<IntentManager> {
|
||||||
|
every { startCustomTabsActivity(any()) } just runs
|
||||||
|
}
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
composeTestRule.setContent {
|
setContentWithBackDispatcher {
|
||||||
ImportLoginsScreen(
|
ImportLoginsScreen(
|
||||||
onNavigateBack = { navigateBackCalled = true },
|
onNavigateBack = { navigateBackCalled = true },
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
|
intentManager = intentManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,9 +176,158 @@ class ImportLoginsScreenTest : BaseComposeTest() {
|
||||||
verifyActionSent(ImportLoginsAction.DismissDialog)
|
verifyActionSent(ImportLoginsAction.DismissDialog)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyActionSent(action: ImportLoginsAction) {
|
@Test
|
||||||
verify { viewModel.trySendAction(action) }
|
fun `OpenHelpLink event is used to open URI with intent manager`() {
|
||||||
|
mutableImportLoginsEventFlow.tryEmit(ImportLoginsEvent.OpenHelpLink)
|
||||||
|
verify {
|
||||||
|
intentManager.startCustomTabsActivity("https://bitwarden.com/help/import-data/".toUri())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_STATE = ImportLoginsState(dialogState = null)
|
@Test
|
||||||
|
fun `while on initial content system back sends CloseClick action`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
backDispatcher?.onBackPressed()
|
||||||
|
verifyActionSent(ImportLoginsAction.CloseClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Step one content is displayed when view state is ImportStepOne`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Step 1 of 3")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `while on step one correct actions are sent when buttons are clicked`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Back")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToInitialContent)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Continue")
|
||||||
|
.performClick()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToStepTwo)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `while on step one system back returns to the previous content`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
backDispatcher?.onBackPressed()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToInitialContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Step two content is displayed when view state is ImportStepTwo`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Step 2 of 3")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `while on step two correct actions are sent when buttons are clicked`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Back")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToStepOne)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Continue")
|
||||||
|
.performClick()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToStepThree)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `while on step two system back returns to the previous content`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
backDispatcher?.onBackPressed()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToStepOne)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Step three content is displayed when view state is ImportStepThree`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Step 3 of 3")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `while on step three correct actions are sent when buttons are clicked`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Back")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToStepTwo)
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Continue")
|
||||||
|
.performClick()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToSyncInProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `while on step three system back returns to the previous content`() {
|
||||||
|
mutableImportLoginsStateFlow.tryEmit(
|
||||||
|
DEFAULT_STATE.copy(
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
backDispatcher?.onBackPressed()
|
||||||
|
verifyActionSent(ImportLoginsAction.MoveToStepTwo)
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Helper methods
|
||||||
|
|
||||||
|
private fun verifyActionSent(action: ImportLoginsAction) {
|
||||||
|
verify { viewModel.trySendAction(action) }
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion Helper methods
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE = ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import app.cash.turbine.turbineScope
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class ImportLoginsViewModelTest : BaseViewModelTest() {
|
class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
|
@ -25,6 +26,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -37,6 +39,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -54,6 +57,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -61,6 +65,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -81,6 +86,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.ImportLater,
|
dialogState = ImportLoginsState.DialogState.ImportLater,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
stateFlow.awaitItem(),
|
stateFlow.awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -88,6 +94,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
stateFlow.awaitItem(),
|
stateFlow.awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -99,7 +106,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ConfirmGetStarted sets dialog state to null`() = runTest {
|
fun `ConfirmGetStarted sets dialog state to null and view state to step one`() = runTest {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
viewModel.stateFlow.test {
|
viewModel.stateFlow.test {
|
||||||
// Initial state
|
// Initial state
|
||||||
|
@ -110,6 +117,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = ImportLoginsState.DialogState.GetStarted,
|
dialogState = ImportLoginsState.DialogState.GetStarted,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -117,6 +125,7 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
ImportLoginsState(
|
ImportLoginsState(
|
||||||
dialogState = null,
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
),
|
),
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
|
@ -135,7 +144,78 @@ class ImportLoginsViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HelpClick sends OpenHelpLink event`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.HelpClick)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsEvent.OpenHelpLink,
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MoveToStepOne sets view state to ImportStepOne`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToStepOne)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepOne,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MoveToStepTwo sets view state to ImportStepTwo`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToStepTwo)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepTwo,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MoveToStepThree sets view state to ImportStepThree`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToStepThree)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.ImportStepThree,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `MoveToInitialContent sets view state to InitialContent`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
// first set to step one
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToStepOne)
|
||||||
|
assertTrue(viewModel.stateFlow.value.viewState is ImportLoginsState.ViewState.ImportStepOne)
|
||||||
|
// now move back to intial
|
||||||
|
viewModel.trySendAction(ImportLoginsAction.MoveToInitialContent)
|
||||||
|
assertEquals(
|
||||||
|
ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel()
|
private fun createViewModel(): ImportLoginsViewModel = ImportLoginsViewModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val DEFAULT_STATE = ImportLoginsState(dialogState = null)
|
private val DEFAULT_STATE = ImportLoginsState(
|
||||||
|
dialogState = null,
|
||||||
|
viewState = ImportLoginsState.ViewState.InitialContent,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue