mirror of
https://github.com/bitwarden/android.git
synced 2024-11-22 01:16:02 +03:00
PM-12772 Add notification action card to security settings when applicable (#4008)
This commit is contained in:
parent
9e4119fe32
commit
8e092ef860
10 changed files with 410 additions and 67 deletions
|
@ -48,7 +48,7 @@ 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.button.BitwardenFilledButton
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
|
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||||
|
@ -213,7 +213,7 @@ private fun CompleteRegistrationContent(
|
||||||
.standardHorizontalMargin(),
|
.standardHorizontalMargin(),
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
BitwardenActionCard(
|
BitwardenActionCardSmall(
|
||||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_tooltip),
|
actionIcon = rememberVectorPainter(id = R.drawable.ic_tooltip),
|
||||||
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
||||||
callToActionText = stringResource(id = R.string.learn_more),
|
callToActionText = stringResource(id = R.string.learn_more),
|
||||||
|
|
|
@ -35,7 +35,7 @@ import com.x8bit.bitwarden.R
|
||||||
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.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.card.BitwardenActionCard
|
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCardSmall
|
||||||
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||||
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
|
||||||
|
@ -158,7 +158,7 @@ private fun TryGeneratorCard(
|
||||||
onCardClicked: () -> Unit,
|
onCardClicked: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
BitwardenActionCard(
|
BitwardenActionCardSmall(
|
||||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
||||||
actionText = stringResource(
|
actionText = stringResource(
|
||||||
R.string.use_the_generator_to_create_a_strong_unique_password,
|
R.string.use_the_generator_to_create_a_strong_unique_password,
|
||||||
|
|
|
@ -1,117 +1,123 @@
|
||||||
package com.x8bit.bitwarden.ui.platform.components.card
|
package com.x8bit.bitwarden.ui.platform.components.card
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
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.vector.VectorPainter
|
import androidx.compose.ui.res.stringResource
|
||||||
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.R
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.badge.NotificationBadge
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A reusable card for displaying actions to the user.
|
* A design component action card, which contains a title, action button, and a dismiss button
|
||||||
|
* by default, with optional leading icon content.
|
||||||
|
*
|
||||||
|
* @param cardTitle The title of the card.
|
||||||
|
* @param actionText The text content on the CTA button.
|
||||||
|
* @param onActionClick The action to perform when the CTA button is clicked.
|
||||||
|
* @param onDismissClick The action to perform when the dismiss button is clicked.
|
||||||
|
* @param leadingContent Optional content to display on the leading side of the
|
||||||
|
* [cardTitle] [Text].
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BitwardenActionCard(
|
fun BitwardenActionCard(
|
||||||
actionIcon: VectorPainter,
|
cardTitle: String,
|
||||||
actionText: String,
|
actionText: String,
|
||||||
callToActionText: String,
|
onActionClick: () -> Unit,
|
||||||
onCardClicked: () -> Unit,
|
onDismissClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
|
leadingContent: @Composable (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
onClick = onCardClicked,
|
|
||||||
shape = RoundedCornerShape(size = 16.dp),
|
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
|
containerColor = BitwardenTheme.colorScheme.background.tertiary,
|
||||||
),
|
),
|
||||||
elevation = CardDefaults.elevatedCardElevation(),
|
elevation = CardDefaults.elevatedCardElevation(),
|
||||||
|
border = BorderStroke(width = 1.dp, color = BitwardenTheme.colorScheme.stroke.border),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth()
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
.padding(16.dp),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = actionIcon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(weight = 1f),
|
|
||||||
) {
|
) {
|
||||||
|
leadingContent?.let {
|
||||||
|
it()
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = actionText,
|
text = cardTitle,
|
||||||
style = BitwardenTheme.typography.bodyLarge,
|
style = BitwardenTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(Modifier.weight(1f))
|
||||||
Text(
|
BitwardenStandardIconButton(
|
||||||
text = callToActionText,
|
painter = rememberVectorPainter(id = R.drawable.ic_close),
|
||||||
style = BitwardenTheme.typography.labelLarge,
|
contentDescription = stringResource(id = R.string.close),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
onClick = onDismissClick,
|
||||||
|
contentColor = BitwardenTheme.colorScheme.icon.primary,
|
||||||
|
modifier = Modifier.offset(x = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Box(
|
BitwardenFilledButton(
|
||||||
modifier = Modifier
|
actionText,
|
||||||
.align(Alignment.CenterVertically),
|
onClick = onActionClick,
|
||||||
) {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
trailingContent?.invoke(this)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ActionCardPreview() {
|
private fun BitwardenActionCard_preview() {
|
||||||
BitwardenTheme {
|
BitwardenTheme {
|
||||||
BitwardenActionCard(
|
BitwardenActionCard(
|
||||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
cardTitle = "Title",
|
||||||
actionText = "This is an action.",
|
actionText = "Action",
|
||||||
callToActionText = "Take action",
|
onActionClick = {},
|
||||||
onCardClicked = { },
|
onDismissClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
|
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ActionCardWithTrailingPreview() {
|
private fun BitwardenActionCardWithLeadingContent_preview() {
|
||||||
BitwardenTheme {
|
BitwardenTheme {
|
||||||
BitwardenActionCard(
|
BitwardenActionCard(
|
||||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
cardTitle = "Title",
|
||||||
actionText = "An action with trailing content",
|
actionText = "Action",
|
||||||
callToActionText = "Take action",
|
onActionClick = {},
|
||||||
onCardClicked = {},
|
onDismissClick = {},
|
||||||
trailingContent = {
|
leadingContent = {
|
||||||
Icon(
|
NotificationBadge(
|
||||||
painter = rememberVectorPainter(id = R.drawable.ic_navigate_next),
|
notificationCount = 1,
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
package com.x8bit.bitwarden.ui.platform.components.card
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
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.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.vector.VectorPainter
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||||
|
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable card for displaying actions to the user.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BitwardenActionCardSmall(
|
||||||
|
actionIcon: VectorPainter,
|
||||||
|
actionText: String,
|
||||||
|
callToActionText: String,
|
||||||
|
onCardClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
onClick = onCardClicked,
|
||||||
|
shape = RoundedCornerShape(size = 16.dp),
|
||||||
|
modifier = modifier,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = BitwardenTheme.colorScheme.background.tertiary,
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.elevatedCardElevation(),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = actionIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = BitwardenTheme.colorScheme.icon.secondary,
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(weight = 1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = actionText,
|
||||||
|
style = BitwardenTheme.typography.bodyLarge,
|
||||||
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = callToActionText,
|
||||||
|
style = BitwardenTheme.typography.labelLarge,
|
||||||
|
color = BitwardenTheme.colorScheme.text.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
) {
|
||||||
|
trailingContent?.invoke(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ActionCardSmall_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
BitwardenActionCardSmall(
|
||||||
|
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
||||||
|
actionText = "This is an action.",
|
||||||
|
callToActionText = "Take action",
|
||||||
|
onCardClicked = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun ActionCardSmallWithTrailingIcon_preview() {
|
||||||
|
BitwardenTheme {
|
||||||
|
BitwardenActionCardSmall(
|
||||||
|
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
||||||
|
actionText = "An action with trailing content",
|
||||||
|
callToActionText = "Take action",
|
||||||
|
onCardClicked = {},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = rememberVectorPainter(id = R.drawable.ic_navigate_next),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -96,8 +96,8 @@ data class SettingsState(
|
||||||
private val securityCount: Int,
|
private val securityCount: Int,
|
||||||
) {
|
) {
|
||||||
val notificationBadgeCountMap: Map<Settings, Int> = mapOf(
|
val notificationBadgeCountMap: Map<Settings, Int> = mapOf(
|
||||||
Settings.ACCOUNT_SECURITY to autoFillCount,
|
Settings.ACCOUNT_SECURITY to securityCount,
|
||||||
Settings.AUTO_FILL to securityCount,
|
Settings.AUTO_FILL to autoFillCount,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
@ -21,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
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.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
@ -36,8 +40,11 @@ import com.x8bit.bitwarden.data.platform.repository.model.VaultTimeoutAction
|
||||||
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.Text
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
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.badge.NotificationBadge
|
||||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||||
|
@ -175,6 +182,33 @@ fun AccountSecurityScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.shouldShowUnlockActionCard,
|
||||||
|
label = "UnlockActionCard",
|
||||||
|
exit = fadeOut() + shrinkVertically(shrinkTowards = Alignment.Top),
|
||||||
|
) {
|
||||||
|
BitwardenActionCard(
|
||||||
|
cardTitle = stringResource(id = R.string.set_up_unlock),
|
||||||
|
actionText = stringResource(R.string.get_started),
|
||||||
|
onActionClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(AccountSecurityAction.UnlockActionCardCtaClick)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(AccountSecurityAction.UnlockActionCardDismiss)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
NotificationBadge(notificationCount = 1)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.standardHorizontalMargin()
|
||||||
|
.padding(top = 12.dp, bottom = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
BitwardenListHeaderText(
|
BitwardenListHeaderText(
|
||||||
label = stringResource(id = R.string.approve_login_requests),
|
label = stringResource(id = R.string.approve_login_requests),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
@ -77,6 +77,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
||||||
vaultTimeoutPolicyMinutes = null,
|
vaultTimeoutPolicyMinutes = null,
|
||||||
vaultTimeoutPolicyAction = null,
|
vaultTimeoutPolicyAction = null,
|
||||||
|
shouldShowUnlockActionCard = false,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
@ -113,6 +114,14 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
|
settingsRepository
|
||||||
|
.getShowUnlockBadgeFlow(state.userId)
|
||||||
|
.map {
|
||||||
|
AccountSecurityAction.Internal.ShowUnlockBadgeUpdated(it)
|
||||||
|
}
|
||||||
|
.onEach(::sendAction)
|
||||||
|
.launchIn(viewModelScope)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
trySendAction(
|
trySendAction(
|
||||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||||
|
@ -146,6 +155,17 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
is AccountSecurityAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
|
is AccountSecurityAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
|
||||||
is AccountSecurityAction.PushNotificationConfirm -> handlePushNotificationConfirm()
|
is AccountSecurityAction.PushNotificationConfirm -> handlePushNotificationConfirm()
|
||||||
is AccountSecurityAction.Internal -> handleInternalAction(action)
|
is AccountSecurityAction.Internal -> handleInternalAction(action)
|
||||||
|
AccountSecurityAction.UnlockActionCardCtaClick -> handleUnlockCardCtaClick()
|
||||||
|
AccountSecurityAction.UnlockActionCardDismiss -> handleUnlockCardDismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUnlockCardDismiss() {
|
||||||
|
dismissUnlockNotificationBadge()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUnlockCardCtaClick() {
|
||||||
|
dismissUnlockNotificationBadge()
|
||||||
|
// TODO: Navigate to unlock set up screen PM-13067
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleAccountFingerprintPhraseClick() {
|
private fun handleAccountFingerprintPhraseClick() {
|
||||||
|
@ -199,6 +219,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dismissUnlockNotificationBadge()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleFingerPrintLearnMoreClick() {
|
private fun handleFingerPrintLearnMoreClick() {
|
||||||
|
@ -310,6 +331,7 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dismissUnlockNotificationBadge()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleInternalAction(action: AccountSecurityAction.Internal) {
|
private fun handleInternalAction(action: AccountSecurityAction.Internal) {
|
||||||
|
@ -329,6 +351,20 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
is AccountSecurityAction.Internal.PolicyUpdateReceive -> {
|
is AccountSecurityAction.Internal.PolicyUpdateReceive -> {
|
||||||
handlePolicyUpdateReceive(action)
|
handlePolicyUpdateReceive(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AccountSecurityAction.Internal.ShowUnlockBadgeUpdated -> {
|
||||||
|
handleShowUnlockBadgeUpdated(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleShowUnlockBadgeUpdated(
|
||||||
|
action: AccountSecurityAction.Internal.ShowUnlockBadgeUpdated,
|
||||||
|
) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
shouldShowUnlockActionCard = action.showUnlockBadge,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,6 +440,14 @@ class AccountSecurityViewModel @Inject constructor(
|
||||||
mutableStateFlow.update { it.copy(vaultTimeoutAction = vaultTimeoutAction) }
|
mutableStateFlow.update { it.copy(vaultTimeoutAction = vaultTimeoutAction) }
|
||||||
settingsRepository.vaultTimeoutAction = vaultTimeoutAction
|
settingsRepository.vaultTimeoutAction = vaultTimeoutAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun dismissUnlockNotificationBadge() {
|
||||||
|
if (!state.shouldShowUnlockActionCard) return
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(
|
||||||
|
userId = state.userId,
|
||||||
|
showBadge = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -423,6 +467,7 @@ data class AccountSecurityState(
|
||||||
val vaultTimeoutAction: VaultTimeoutAction,
|
val vaultTimeoutAction: VaultTimeoutAction,
|
||||||
val vaultTimeoutPolicyMinutes: Int?,
|
val vaultTimeoutPolicyMinutes: Int?,
|
||||||
val vaultTimeoutPolicyAction: String?,
|
val vaultTimeoutPolicyAction: String?,
|
||||||
|
val shouldShowUnlockActionCard: Boolean,
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
/**
|
/**
|
||||||
* Indicates that there is a mechanism for unlocking your vault in place.
|
* Indicates that there is a mechanism for unlocking your vault in place.
|
||||||
|
@ -633,6 +678,16 @@ sealed class AccountSecurityAction {
|
||||||
val unlockWithPinState: UnlockWithPinState,
|
val unlockWithPinState: UnlockWithPinState,
|
||||||
) : AccountSecurityAction()
|
) : AccountSecurityAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has dismissed the unlock action card.
|
||||||
|
*/
|
||||||
|
data object UnlockActionCardDismiss : AccountSecurityAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User has clicked the CTA on the unlock action card.
|
||||||
|
*/
|
||||||
|
data object UnlockActionCardCtaClick : AccountSecurityAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Models actions that can be sent by the view model itself.
|
* Models actions that can be sent by the view model itself.
|
||||||
*/
|
*/
|
||||||
|
@ -665,5 +720,10 @@ sealed class AccountSecurityAction {
|
||||||
data class PolicyUpdateReceive(
|
data class PolicyUpdateReceive(
|
||||||
val vaultTimeoutPolicies: List<PolicyInformation.VaultTimeout>?,
|
val vaultTimeoutPolicies: List<PolicyInformation.VaultTimeout>?,
|
||||||
) : Internal()
|
) : Internal()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The show unlock badge update has been received.
|
||||||
|
*/
|
||||||
|
data class ShowUnlockBadgeUpdated(val showUnlockBadge: Boolean) : Internal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1009,4 +1009,5 @@ Do you want to switch to this account?</string>
|
||||||
<string name="error_connecting_with_the_duo_service_use_a_different_two_step_login_method_or_contact_duo_for_assistance">Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance.</string>
|
<string name="error_connecting_with_the_duo_service_use_a_different_two_step_login_method_or_contact_duo_for_assistance">Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance.</string>
|
||||||
<string name="master_password_hint_not_specified">Master password hint</string>
|
<string name="master_password_hint_not_specified">Master password hint</string>
|
||||||
<string name="master_password_important_hint">Important: Your master password cannot be recovered if you forget it! 12 characters minimum.</string>
|
<string name="master_password_important_hint">Important: Your master password cannot be recovered if you forget it! 12 characters minimum.</string>
|
||||||
|
<string name="get_started">Get started</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1488,6 +1488,42 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
||||||
.performClick()
|
.performClick()
|
||||||
verify { viewModel.trySendAction(AccountSecurityAction.AuthenticatorSyncToggle(true)) }
|
verify { viewModel.trySendAction(AccountSecurityAction.AuthenticatorSyncToggle(true)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `unlock action card should show when state is true and hide when false`() {
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Get started")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowUnlockActionCard = true) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Get started")
|
||||||
|
.assertIsDisplayed()
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowUnlockActionCard = false) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Get started")
|
||||||
|
.assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when unlock action card is visible clicking the cta button should send correct action`() {
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowUnlockActionCard = true) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithText("Get started")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify { viewModel.trySendAction(AccountSecurityAction.UnlockActionCardCtaClick) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when unlock action card is visible clicking dismissing should send correct action`() {
|
||||||
|
mutableStateFlow.update { DEFAULT_STATE.copy(shouldShowUnlockActionCard = true) }
|
||||||
|
composeTestRule
|
||||||
|
.onNodeWithContentDescription("Close")
|
||||||
|
.performScrollTo()
|
||||||
|
.performClick()
|
||||||
|
verify { viewModel.trySendAction(AccountSecurityAction.UnlockActionCardDismiss) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val CIPHER = mockk<Cipher>()
|
private val CIPHER = mockk<Cipher>()
|
||||||
|
@ -1505,4 +1541,5 @@ private val DEFAULT_STATE = AccountSecurityState(
|
||||||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||||
vaultTimeoutPolicyMinutes = null,
|
vaultTimeoutPolicyMinutes = null,
|
||||||
vaultTimeoutPolicyAction = null,
|
vaultTimeoutPolicyAction = null,
|
||||||
|
shouldShowUnlockActionCard = false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.encodeToJsonElement
|
import kotlinx.serialization.json.encodeToJsonElement
|
||||||
|
@ -45,6 +46,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import javax.crypto.Cipher
|
import javax.crypto.Cipher
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||||
|
@ -53,6 +55,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
every { userStateFlow } returns mutableUserStateFlow
|
every { userStateFlow } returns mutableUserStateFlow
|
||||||
}
|
}
|
||||||
private val vaultRepository: VaultRepository = mockk(relaxed = true)
|
private val vaultRepository: VaultRepository = mockk(relaxed = true)
|
||||||
|
private val mutableShowUnlockBadgeFlow = MutableStateFlow(false)
|
||||||
private val settingsRepository: SettingsRepository = mockk {
|
private val settingsRepository: SettingsRepository = mockk {
|
||||||
every { isAuthenticatorSyncEnabled } returns false
|
every { isAuthenticatorSyncEnabled } returns false
|
||||||
every { isUnlockWithBiometricsEnabled } returns false
|
every { isUnlockWithBiometricsEnabled } returns false
|
||||||
|
@ -60,6 +63,8 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
||||||
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
|
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
|
||||||
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
coEvery { getUserFingerprint() } returns UserFingerprintResult.Success(FINGERPRINT)
|
||||||
|
every { getShowUnlockBadgeFlow(any()) } returns mutableShowUnlockBadgeFlow
|
||||||
|
every { storeShowUnlockSettingBadge(any(), false) } just runs
|
||||||
}
|
}
|
||||||
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
private val mutableActivePolicyFlow = bufferedMutableSharedFlow<List<SyncResponseJson.Policy>>()
|
||||||
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
||||||
|
@ -359,6 +364,21 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
awaitItem(),
|
awaitItem(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(DEFAULT_STATE.userId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on EnableBiometricsClick should update user show unlock badge status if shown`() {
|
||||||
|
mutableShowUnlockBadgeFlow.update { true }
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(AccountSecurityAction.EnableBiometricsClick)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(DEFAULT_STATE.userId, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -576,6 +596,41 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
shouldRequireMasterPasswordOnRestart = true,
|
shouldRequireMasterPasswordOnRestart = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(DEFAULT_STATE.userId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `on UnlockWithPinToggle Enabled should update show unlock badge state if card is visible`() {
|
||||||
|
mutableShowUnlockBadgeFlow.update { true }
|
||||||
|
val initialState = DEFAULT_STATE.copy(
|
||||||
|
isUnlockWithPinEnabled = false,
|
||||||
|
)
|
||||||
|
every { settingsRepository.storeUnlockPin(any(), any()) } just runs
|
||||||
|
|
||||||
|
val viewModel = createViewModel(initialState = initialState)
|
||||||
|
viewModel.trySendAction(
|
||||||
|
AccountSecurityAction.UnlockWithPinToggle(
|
||||||
|
UnlockWithPinState.Enabled(
|
||||||
|
pin = "1234",
|
||||||
|
shouldRequireMasterPasswordOnRestart = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(isUnlockWithPinEnabled = true, shouldShowUnlockActionCard = true),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
verify {
|
||||||
|
settingsRepository.storeUnlockPin(
|
||||||
|
pin = "1234",
|
||||||
|
shouldRequireMasterPasswordOnRestart = true,
|
||||||
|
)
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(DEFAULT_STATE.userId, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -652,6 +707,36 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when showUnlockBadgeFlow updates value, should update state`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.stateFlow.test {
|
||||||
|
assertEquals(DEFAULT_STATE, awaitItem())
|
||||||
|
mutableShowUnlockBadgeFlow.update { true }
|
||||||
|
assertEquals(DEFAULT_STATE.copy(shouldShowUnlockActionCard = true), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when UnlockActionCardDismiss action received, should dismiss unlock action card`() {
|
||||||
|
mutableShowUnlockBadgeFlow.update { true }
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(AccountSecurityAction.UnlockActionCardDismiss)
|
||||||
|
verify {
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(DEFAULT_STATE.userId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when UnlockActionCardCtaClick action received, should dismiss unlock action card`() {
|
||||||
|
mutableShowUnlockBadgeFlow.update { true }
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.trySendAction(AccountSecurityAction.UnlockActionCardCtaClick)
|
||||||
|
verify {
|
||||||
|
settingsRepository.storeShowUnlockSettingBadge(DEFAULT_STATE.userId, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("LongParameterList")
|
||||||
private fun createViewModel(
|
private fun createViewModel(
|
||||||
initialState: AccountSecurityState? = DEFAULT_STATE,
|
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||||
|
@ -716,4 +801,5 @@ private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
||||||
vaultTimeoutPolicyMinutes = null,
|
vaultTimeoutPolicyMinutes = null,
|
||||||
vaultTimeoutPolicyAction = null,
|
vaultTimeoutPolicyAction = null,
|
||||||
shouldShowEnableAuthenticatorSync = false,
|
shouldShowEnableAuthenticatorSync = false,
|
||||||
|
shouldShowUnlockActionCard = false,
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue