PM-10620 prevent account lockout tips screen (#3711)

This commit is contained in:
Dave Severns 2024-08-12 08:38:23 -04:00 committed by GitHub
parent 5e643e11fd
commit 2b13151bd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 338 additions and 2 deletions

View file

@ -23,6 +23,7 @@ import com.x8bit.bitwarden.ui.auth.feature.loginwithdevice.navigateToLoginWithDe
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordguidance.masterPasswordGuidanceDestination
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHintDestination
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
import com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout.preventAccountLockoutDestination
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
import com.x8bit.bitwarden.ui.auth.feature.setpassword.setPasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
@ -130,6 +131,9 @@ fun NavGraphBuilder.authGraph(
// TODO [PM-10619](https://bitwarden.atlassian.net/browse/PM-10619)
},
)
preventAccountLockoutDestination(
onNavigateBack = { navController.popBackStack() },
)
}
}

View file

@ -56,7 +56,6 @@ fun MasterPasswordGuidanceScreen(
onNavigateToGeneratePassword: () -> Unit,
viewModel: MasterPasswordGuidanceViewModel = hiltViewModel(),
) {
EventsEffect(viewModel = viewModel) { event ->
when (event) {
MasterPasswordGuidanceEvent.NavigateBack -> onNavigateBack()
@ -118,7 +117,7 @@ fun MasterPasswordGuidanceScreen(
),
)
}
HorizontalDivider()
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
Column(
modifier = Modifier
.fillMaxWidth()

View file

@ -0,0 +1,30 @@
package com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val PREVENT_ACCOUNT_LOCKOUT = "prevent_account_lockout"
/**
* Navigate to prevent account lockout screen.
*/
fun NavController.navigateToPreventAccountLockout(navOptions: NavOptions? = null) {
this.navigate(PREVENT_ACCOUNT_LOCKOUT, navOptions)
}
/**
* Add the prevent account lockout screen to the nav graph.
*/
fun NavGraphBuilder.preventAccountLockoutDestination(
onNavigateBack: () -> Unit,
) {
composableWithSlideTransitions(
route = PREVENT_ACCOUNT_LOCKOUT,
) {
PreventAccountLockoutScreen(
onNavigateBack = onNavigateBack,
)
}
}

View file

@ -0,0 +1,185 @@
package com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
/**
* Top level screen component for the prevent account lockout info screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun PreventAccountLockoutScreen(
onNavigateBack: () -> Unit,
viewModel: PreventAccountLockoutViewModel = hiltViewModel(),
) {
EventsEffect(viewModel = viewModel) { event ->
when (event) {
PreventAccountLockoutEvent.NavigateBack -> onNavigateBack()
}
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(R.string.prevent_account_lockout),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{
viewModel.trySendAction(PreventAccountLockoutAction.CloseClickAction)
}
},
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.fillMaxWidth()
.standardHorizontalMargin()
.verticalScroll(rememberScrollState()),
) {
NeverLoseAccessContent()
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@Composable
private fun NeverLoseAccessContent(
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(size = 4.dp))
.background(MaterialTheme.colorScheme.surfaceContainerLowest),
) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.never_lose_access_to_your_vault),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 24.dp),
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(
R.string.the_best_way_to_make_sure_you_can_always_access_your_account,
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 24.dp),
)
Spacer(modifier = Modifier.height(24.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
Spacer(modifier = Modifier.height(16.dp))
AccountRecoveryTipRow(
title = stringResource(R.string.create_a_hint),
description = stringResource(
R.string.your_hint_will_be_send_to_you_via_email_when_you_request_it,
),
icon = rememberVectorPainter(id = R.drawable.ic_light_bulb),
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outline)
Spacer(modifier = Modifier.height(16.dp))
AccountRecoveryTipRow(
title = stringResource(R.string.write_your_password_down),
description = stringResource(R.string.keep_it_secret_keep_it_safe),
icon = rememberVectorPainter(id = R.drawable.ic_edit),
modifier = Modifier.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
}
}
@Composable
private fun AccountRecoveryTipRow(
title: String,
description: String,
icon: Painter,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(32.dp)
.clearAndSetSemantics { },
)
Spacer(modifier = Modifier.width(8.dp))
Column {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
@Preview
@Composable
private fun PreventAccountLockoutScreenPreview() {
BitwardenTheme {
PreventAccountLockoutScreen(onNavigateBack = {})
}
}

View file

@ -0,0 +1,45 @@
package com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
/**
* ViewModel for the [PreventAccountLockoutScreen].
*/
@HiltViewModel
class PreventAccountLockoutViewModel @Inject constructor() :
BaseViewModel<Unit, PreventAccountLockoutEvent, PreventAccountLockoutAction>(
initialState = Unit,
) {
override fun handleAction(action: PreventAccountLockoutAction) {
when (action) {
PreventAccountLockoutAction.CloseClickAction -> handleCloseClickAction()
}
}
private fun handleCloseClickAction() = sendEvent(PreventAccountLockoutEvent.NavigateBack)
}
/**
* Model events to send to the [PreventAccountLockoutScreen].
*/
sealed class PreventAccountLockoutEvent {
/**
* Navigates to the previous screen.
*/
data object NavigateBack : PreventAccountLockoutEvent()
}
/**
* Model actions to be handled in the [PreventAccountLockoutViewModel].
*/
sealed class PreventAccountLockoutAction {
/**
* Close button has been clicked.
*/
data object CloseClickAction : PreventAccountLockoutAction()
}

View file

@ -952,4 +952,11 @@ Do you want to switch to this account?</string>
<string name="you_can_return_to_complete_this_step_anytime_from_account_security_in_settings">You can return to complete this step anytime from Account Security in Settings.</string>
<string name="confirm">Confirm</string>
<string name="set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins">Set up biometrics or choose a PIN code to quickly access your vault and AutoFill your logins.</string>
<string name="never_lose_access_to_your_vault">Never lose access to your vault</string>
<string name="the_best_way_to_make_sure_you_can_always_access_your_account">The best way to make sure you can always access your account is to set up safeguards from the start.</string>
<string name="create_a_hint">Create a hint</string>
<string name="your_hint_will_be_send_to_you_via_email_when_you_request_it">Your hint will be sent to you via email when you request it.</string>
<string name="write_your_password_down">Write your password down</string>
<string name="keep_it_secret_keep_it_safe">Be careful to keep your written password somewhere secret and safe.</string>
<string name="prevent_account_lockout">Prevent account lockout</string>
</resources>

View file

@ -0,0 +1,46 @@
package com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class PreventAccountLockoutScreenTest : BaseComposeTest() {
private var onBackHasBeenInvoked = false
private val mutableEventFlow = bufferedMutableSharedFlow<PreventAccountLockoutEvent>()
private val viewModel = mockk<PreventAccountLockoutViewModel>(relaxed = true) {
every { eventFlow } returns mutableEventFlow
}
@Before
fun setup() {
composeTestRule.setContent {
PreventAccountLockoutScreen(
onNavigateBack = { onBackHasBeenInvoked = true },
viewModel = viewModel,
)
}
}
@Test
fun `When navigation button is clicked CloseClickAction is sent`() {
composeTestRule
.onNodeWithContentDescription("Close")
.performClick()
verify { viewModel.trySendAction(PreventAccountLockoutAction.CloseClickAction) }
}
@Test
fun `NavigateBackEvent from ViewModel invokes onBackNavigation lambda`() {
mutableEventFlow.tryEmit(PreventAccountLockoutEvent.NavigateBack)
assertTrue(onBackHasBeenInvoked)
}
}

View file

@ -0,0 +1,20 @@
package com.x8bit.bitwarden.ui.auth.feature.preventaccountlockout
import app.cash.turbine.test
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class PreventAccountLockoutViewModelTest : BaseViewModelTest() {
@Test
fun `When handling CloseClickAction a NavigateBack event is emitted`() = runTest {
val viewModel = PreventAccountLockoutViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(PreventAccountLockoutAction.CloseClickAction)
assertEquals(PreventAccountLockoutEvent.NavigateBack, awaitItem())
}
}
}