PM-12772 Add notification action card to security settings when applicable (#4008)

This commit is contained in:
Dave Severns 2024-10-02 08:27:45 -04:00 committed by GitHub
parent 9e4119fe32
commit 8e092ef860
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 410 additions and 67 deletions

View file

@ -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),

View file

@ -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,

View file

@ -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,
)
},
)

View file

@ -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,
)
},
)
}
}

View file

@ -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,
)
}

View file

@ -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

View file

@ -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()
}
}

View file

@ -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>

View file

@ -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,
)

View file

@ -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,
)