mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +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.button.BitwardenFilledButton
|
||||
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.BitwardenLoadingDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
|
@ -213,7 +213,7 @@ private fun CompleteRegistrationContent(
|
|||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenActionCard(
|
||||
BitwardenActionCardSmall(
|
||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_tooltip),
|
||||
actionText = stringResource(id = R.string.what_makes_a_password_strong),
|
||||
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.standardHorizontalMargin
|
||||
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.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
|
@ -158,7 +158,7 @@ private fun TryGeneratorCard(
|
|||
onCardClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BitwardenActionCard(
|
||||
BitwardenActionCardSmall(
|
||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
||||
actionText = stringResource(
|
||||
R.string.use_the_generator_to_create_a_strong_unique_password,
|
||||
|
|
|
@ -1,117 +1,123 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components.card
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
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.offset
|
||||
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.res.stringResource
|
||||
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.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.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
|
||||
fun BitwardenActionCard(
|
||||
actionIcon: VectorPainter,
|
||||
cardTitle: String,
|
||||
actionText: String,
|
||||
callToActionText: String,
|
||||
onCardClicked: () -> Unit,
|
||||
onActionClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
trailingContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||
leadingContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Card(
|
||||
onClick = onCardClicked,
|
||||
shape = RoundedCornerShape(size = 16.dp),
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLowest,
|
||||
containerColor = BitwardenTheme.colorScheme.background.tertiary,
|
||||
),
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
border = BorderStroke(width = 1.dp, color = BitwardenTheme.colorScheme.stroke.border),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = actionIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(24.dp),
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
leadingContent?.let {
|
||||
it()
|
||||
Spacer(Modifier.width(12.dp))
|
||||
}
|
||||
Text(
|
||||
text = cardTitle,
|
||||
style = BitwardenTheme.typography.titleMedium,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
BitwardenStandardIconButton(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_close),
|
||||
contentDescription = stringResource(id = R.string.close),
|
||||
onClick = onDismissClick,
|
||||
contentColor = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.offset(x = 8.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
BitwardenFilledButton(
|
||||
actionText,
|
||||
onClick = onActionClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier.weight(weight = 1f),
|
||||
) {
|
||||
Text(
|
||||
text = actionText,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = callToActionText,
|
||||
style = BitwardenTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically),
|
||||
) {
|
||||
trailingContent?.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ActionCardPreview() {
|
||||
private fun BitwardenActionCard_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenActionCard(
|
||||
actionIcon = rememberVectorPainter(id = R.drawable.ic_generator),
|
||||
actionText = "This is an action.",
|
||||
callToActionText = "Take action",
|
||||
onCardClicked = { },
|
||||
cardTitle = "Title",
|
||||
actionText = "Action",
|
||||
onActionClick = {},
|
||||
onDismissClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun ActionCardWithTrailingPreview() {
|
||||
private fun BitwardenActionCardWithLeadingContent_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenActionCard(
|
||||
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,
|
||||
cardTitle = "Title",
|
||||
actionText = "Action",
|
||||
onActionClick = {},
|
||||
onDismissClick = {},
|
||||
leadingContent = {
|
||||
NotificationBadge(
|
||||
notificationCount = 1,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
) {
|
||||
val notificationBadgeCountMap: Map<Settings, Int> = mapOf(
|
||||
Settings.ACCOUNT_SECURITY to autoFillCount,
|
||||
Settings.AUTO_FILL to securityCount,
|
||||
Settings.ACCOUNT_SECURITY to securityCount,
|
||||
Settings.AUTO_FILL to autoFillCount,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity
|
||||
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -21,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
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.Text
|
||||
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.badge.NotificationBadge
|
||||
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.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
|
@ -175,6 +182,33 @@ fun AccountSecurityScreen(
|
|||
.fillMaxSize()
|
||||
.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(
|
||||
label = stringResource(id = R.string.approve_login_requests),
|
||||
modifier = Modifier
|
||||
|
|
|
@ -77,6 +77,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
vaultTimeoutAction = settingsRepository.vaultTimeoutAction,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
shouldShowUnlockActionCard = false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
@ -113,6 +114,14 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
settingsRepository
|
||||
.getShowUnlockBadgeFlow(state.userId)
|
||||
.map {
|
||||
AccountSecurityAction.Internal.ShowUnlockBadgeUpdated(it)
|
||||
}
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
trySendAction(
|
||||
AccountSecurityAction.Internal.FingerprintResultReceive(
|
||||
|
@ -146,6 +155,17 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
is AccountSecurityAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
|
||||
is AccountSecurityAction.PushNotificationConfirm -> handlePushNotificationConfirm()
|
||||
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() {
|
||||
|
@ -199,6 +219,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
dismissUnlockNotificationBadge()
|
||||
}
|
||||
|
||||
private fun handleFingerPrintLearnMoreClick() {
|
||||
|
@ -310,6 +331,7 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
dismissUnlockNotificationBadge()
|
||||
}
|
||||
|
||||
private fun handleInternalAction(action: AccountSecurityAction.Internal) {
|
||||
|
@ -329,6 +351,20 @@ class AccountSecurityViewModel @Inject constructor(
|
|||
is AccountSecurityAction.Internal.PolicyUpdateReceive -> {
|
||||
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) }
|
||||
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 vaultTimeoutPolicyMinutes: Int?,
|
||||
val vaultTimeoutPolicyAction: String?,
|
||||
val shouldShowUnlockActionCard: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Indicates that there is a mechanism for unlocking your vault in place.
|
||||
|
@ -633,6 +678,16 @@ sealed class AccountSecurityAction {
|
|||
val unlockWithPinState: UnlockWithPinState,
|
||||
) : 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.
|
||||
*/
|
||||
|
@ -665,5 +720,10 @@ sealed class AccountSecurityAction {
|
|||
data class PolicyUpdateReceive(
|
||||
val vaultTimeoutPolicies: List<PolicyInformation.VaultTimeout>?,
|
||||
) : 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="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="get_started">Get started</string>
|
||||
</resources>
|
||||
|
|
|
@ -1488,6 +1488,42 @@ class AccountSecurityScreenTest : BaseComposeTest() {
|
|||
.performClick()
|
||||
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>()
|
||||
|
@ -1505,4 +1541,5 @@ private val DEFAULT_STATE = AccountSecurityState(
|
|||
vaultTimeoutAction = VaultTimeoutAction.LOCK,
|
||||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
shouldShowUnlockActionCard = false,
|
||||
)
|
||||
|
|
|
@ -37,6 +37,7 @@ import io.mockk.runs
|
|||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
|
@ -45,6 +46,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
|||
import org.junit.jupiter.api.Test
|
||||
import javax.crypto.Cipher
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class AccountSecurityViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
@ -53,6 +55,7 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val vaultRepository: VaultRepository = mockk(relaxed = true)
|
||||
private val mutableShowUnlockBadgeFlow = MutableStateFlow(false)
|
||||
private val settingsRepository: SettingsRepository = mockk {
|
||||
every { isAuthenticatorSyncEnabled } returns false
|
||||
every { isUnlockWithBiometricsEnabled } returns false
|
||||
|
@ -60,6 +63,8 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
every { vaultTimeout } returns VaultTimeout.ThirtyMinutes
|
||||
every { vaultTimeoutAction } returns VaultTimeoutAction.LOCK
|
||||
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 biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
||||
|
@ -359,6 +364,21 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
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
|
||||
|
@ -576,6 +596,41 @@ class AccountSecurityViewModelTest : BaseViewModelTest() {
|
|||
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
|
||||
|
@ -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")
|
||||
private fun createViewModel(
|
||||
initialState: AccountSecurityState? = DEFAULT_STATE,
|
||||
|
@ -716,4 +801,5 @@ private val DEFAULT_STATE: AccountSecurityState = AccountSecurityState(
|
|||
vaultTimeoutPolicyMinutes = null,
|
||||
vaultTimeoutPolicyAction = null,
|
||||
shouldShowEnableAuthenticatorSync = false,
|
||||
shouldShowUnlockActionCard = false,
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue