mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 19:28:44 +03:00
PM-10621: Add the SetupUnlockScreen (#3699)
This commit is contained in:
parent
6bb5ef7417
commit
145f8adf0c
9 changed files with 1179 additions and 0 deletions
|
@ -0,0 +1,30 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithPushTransitions
|
||||
|
||||
private const val SETUP_UNLOCK_ROUTE = "setup_unlock"
|
||||
|
||||
/**
|
||||
* Navigate to the setup unlock screen.
|
||||
*/
|
||||
fun NavController.navigateToSetupUnlockScreen(navOptions: NavOptions? = null) {
|
||||
this.navigate(SETUP_UNLOCK_ROUTE, navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the setup unlock screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.setupUnlockDestination(
|
||||
onNavigateToSetupAutofill: () -> Unit,
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = SETUP_UNLOCK_ROUTE,
|
||||
) {
|
||||
SetupUnlockScreen(
|
||||
onNavigateToSetupAutofill = onNavigateToSetupAutofill,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
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.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
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.getValue
|
||||
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.LocalConfiguration
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers.SetupUnlockHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
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.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithBiometricsSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.BitwardenUnlockWithPinSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.util.isPortrait
|
||||
|
||||
/**
|
||||
* Top level composable for the setup unlock screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SetupUnlockScreen(
|
||||
onNavigateToSetupAutofill: () -> Unit,
|
||||
viewModel: SetupUnlockViewModel = hiltViewModel(),
|
||||
biometricsManager: BiometricsManager = LocalBiometricsManager.current,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = remember(viewModel) { SetupUnlockHandler.create(viewModel = viewModel) }
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
SetupUnlockEvent.NavigateToSetupAutofill -> onNavigateToSetupAutofill()
|
||||
}
|
||||
}
|
||||
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(id = R.string.account_setup),
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = null,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
SetupUnlockScreenContent(
|
||||
state = state,
|
||||
handler = handler,
|
||||
biometricsManager = biometricsManager,
|
||||
modifier = Modifier
|
||||
.padding(paddingValues = innerPadding)
|
||||
.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetupUnlockScreenContent(
|
||||
state: SetupUnlockState,
|
||||
handler: SetupUnlockHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
biometricsManager: BiometricsManager,
|
||||
config: Configuration = LocalConfiguration.current,
|
||||
) {
|
||||
val marginHorizontal = remember(config.orientation) {
|
||||
if (config.isPortrait) 16.dp else 48.dp
|
||||
}
|
||||
Column(
|
||||
modifier = modifier.verticalScroll(state = rememberScrollState()),
|
||||
) {
|
||||
if (config.isPortrait) {
|
||||
SetupUnlockHeaderPortrait()
|
||||
} else {
|
||||
SetupUnlockHeaderLandscape()
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
BitwardenUnlockWithBiometricsSwitch(
|
||||
isBiometricsSupported = biometricsManager.isBiometricsSupported,
|
||||
isChecked = state.isUnlockWithBiometricsEnabled,
|
||||
onDisableBiometrics = handler.onDisableBiometrics,
|
||||
onEnableBiometrics = handler.onEnableBiometrics,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "UnlockWithBiometricsSwitch")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = marginHorizontal),
|
||||
)
|
||||
BitwardenUnlockWithPinSwitch(
|
||||
isUnlockWithPasswordEnabled = state.isUnlockWithPasswordEnabled,
|
||||
isUnlockWithPinEnabled = state.isUnlockWithPinEnabled,
|
||||
onUnlockWithPinToggleAction = handler.onUnlockWithPinToggle,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "UnlockWithPinSwitch")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = marginHorizontal),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
onClick = handler.onContinueClick,
|
||||
isEnabled = state.isContinueButtonEnabled,
|
||||
modifier = Modifier
|
||||
.testTag(tag = "ContinueButton")
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = marginHorizontal),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
SetUpLaterButton(
|
||||
onConfirmClick = handler.onSetUpLaterClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = marginHorizontal),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetUpLaterButton(
|
||||
modifier: Modifier,
|
||||
onConfirmClick: () -> Unit,
|
||||
) {
|
||||
var displayConfirmation by rememberSaveable { mutableStateOf(value = false) }
|
||||
if (displayConfirmation) {
|
||||
@Suppress("MaxLineLength")
|
||||
BitwardenTwoButtonDialog(
|
||||
title = stringResource(id = R.string.set_up_unlock_later),
|
||||
message = stringResource(
|
||||
id = R.string.you_can_return_to_complete_this_step_anytime_from_account_security_in_settings,
|
||||
),
|
||||
confirmButtonText = stringResource(id = R.string.confirm),
|
||||
dismissButtonText = stringResource(id = R.string.cancel),
|
||||
onConfirmClick = {
|
||||
onConfirmClick()
|
||||
displayConfirmation = false
|
||||
},
|
||||
onDismissClick = { displayConfirmation = false },
|
||||
onDismissRequest = { displayConfirmation = false },
|
||||
)
|
||||
}
|
||||
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.set_up_later),
|
||||
onClick = { displayConfirmation = true },
|
||||
modifier = modifier.testTag(tag = "SetUpLaterButton"),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.SetupUnlockHeaderPortrait() {
|
||||
Spacer(modifier = Modifier.height(height = 32.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.account_setup),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.size(size = 100.dp)
|
||||
.align(alignment = Alignment.CenterHorizontally),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 24.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.set_up_unlock),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetupUnlockHeaderLandscape(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(horizontal = 112.dp)
|
||||
.padding(horizontal = 48.dp),
|
||||
) {
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.account_setup),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = 100.dp)
|
||||
.align(alignment = Alignment.CenterVertically),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(width = 24.dp))
|
||||
Column(
|
||||
modifier = Modifier.align(alignment = Alignment.CenterVertically),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.set_up_unlock),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
@Suppress("MaxLineLength")
|
||||
Text(
|
||||
text = stringResource(
|
||||
id = R.string.set_up_biometrics_or_choose_a_pin_code_to_quickly_access_your_vault_and_autofill_your_logins,
|
||||
),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* Models logic for the setup unlock screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class SetupUnlockViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val authRepository: AuthRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager,
|
||||
) : BaseViewModel<SetupUnlockState, SetupUnlockEvent, SetupUnlockAction>(
|
||||
initialState = savedStateHandle[KEY_STATE] ?: run {
|
||||
val userId = requireNotNull(authRepository.userStateFlow.value).activeUserId
|
||||
val isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(
|
||||
userId = userId,
|
||||
cipher = biometricsEncryptionManager.getOrCreateCipher(userId = userId),
|
||||
)
|
||||
SetupUnlockState(
|
||||
isUnlockWithPasswordEnabled = authRepository
|
||||
.userStateFlow
|
||||
.value
|
||||
?.activeAccount
|
||||
?.hasMasterPassword != false,
|
||||
isUnlockWithPinEnabled = settingsRepository.isUnlockWithPinEnabled,
|
||||
isUnlockWithBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled &&
|
||||
isBiometricsValid,
|
||||
)
|
||||
},
|
||||
) {
|
||||
override fun handleAction(action: SetupUnlockAction) {
|
||||
when (action) {
|
||||
SetupUnlockAction.ContinueClick -> handleContinueClick()
|
||||
SetupUnlockAction.EnableBiometricsClick -> handleEnableBiometricsClick()
|
||||
SetupUnlockAction.SetUpLaterClick -> handleSetUpLaterClick()
|
||||
is SetupUnlockAction.UnlockWithBiometricToggle -> {
|
||||
handleUnlockWithBiometricToggle(action)
|
||||
}
|
||||
|
||||
is SetupUnlockAction.UnlockWithPinToggle -> handleUnlockWithPinToggle(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleContinueClick() {
|
||||
sendEvent(SetupUnlockEvent.NavigateToSetupAutofill)
|
||||
}
|
||||
|
||||
private fun handleEnableBiometricsClick() {
|
||||
// TODO: Handle biometric unlocking logic PM-10624
|
||||
}
|
||||
|
||||
private fun handleSetUpLaterClick() {
|
||||
sendEvent(SetupUnlockEvent.NavigateToSetupAutofill)
|
||||
}
|
||||
|
||||
private fun handleUnlockWithBiometricToggle(
|
||||
action: SetupUnlockAction.UnlockWithBiometricToggle,
|
||||
) {
|
||||
// TODO: Handle biometric unlocking logic PM-10624
|
||||
}
|
||||
|
||||
private fun handleUnlockWithPinToggle(action: SetupUnlockAction.UnlockWithPinToggle) {
|
||||
// TODO: Handle pin unlocking logic PM-10628
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the UI state for the setup unlock screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class SetupUnlockState(
|
||||
val isUnlockWithPasswordEnabled: Boolean,
|
||||
val isUnlockWithPinEnabled: Boolean,
|
||||
val isUnlockWithBiometricsEnabled: Boolean,
|
||||
) : Parcelable {
|
||||
/**
|
||||
* Indicates whether the continue button should be enabled or disabled.
|
||||
*/
|
||||
val isContinueButtonEnabled: Boolean
|
||||
get() = isUnlockWithBiometricsEnabled || isUnlockWithPinEnabled
|
||||
}
|
||||
|
||||
/**
|
||||
* Models events for the setup unlock screen.
|
||||
*/
|
||||
sealed class SetupUnlockEvent {
|
||||
/**
|
||||
* Navigate to autofill setup.
|
||||
*/
|
||||
data object NavigateToSetupAutofill : SetupUnlockEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models action for the setup unlock screen.
|
||||
*/
|
||||
sealed class SetupUnlockAction {
|
||||
/**
|
||||
* User toggled the unlock with biometrics switch.
|
||||
*/
|
||||
data class UnlockWithBiometricToggle(
|
||||
val isEnabled: Boolean,
|
||||
) : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* The user clicked to enable biometrics.
|
||||
*/
|
||||
data object EnableBiometricsClick : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* User toggled the unlock with pin switch.
|
||||
*/
|
||||
data class UnlockWithPinToggle(
|
||||
val state: UnlockWithPinState,
|
||||
) : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* The user clicked the continue button.
|
||||
*/
|
||||
data object ContinueClick : SetupUnlockAction()
|
||||
|
||||
/**
|
||||
* The user clicked the set up later button.
|
||||
*/
|
||||
data object SetUpLaterClick : SetupUnlockAction()
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockAction
|
||||
import com.x8bit.bitwarden.ui.auth.feature.accountsetup.SetupUnlockViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of the Setup Unlock
|
||||
* Screen.
|
||||
*/
|
||||
data class SetupUnlockHandler(
|
||||
val onDisableBiometrics: () -> Unit,
|
||||
val onEnableBiometrics: () -> Unit,
|
||||
val onUnlockWithPinToggle: (UnlockWithPinState) -> Unit,
|
||||
val onContinueClick: () -> Unit,
|
||||
val onSetUpLaterClick: () -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates an instance of [SetupUnlockHandler] by binding actions to the provided
|
||||
* [SetupUnlockViewModel].
|
||||
*/
|
||||
fun create(viewModel: SetupUnlockViewModel): SetupUnlockHandler =
|
||||
SetupUnlockHandler(
|
||||
onDisableBiometrics = {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = false),
|
||||
)
|
||||
},
|
||||
onEnableBiometrics = {
|
||||
viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick)
|
||||
},
|
||||
onUnlockWithPinToggle = {
|
||||
viewModel.trySendAction(SetupUnlockAction.UnlockWithPinToggle(it))
|
||||
},
|
||||
onContinueClick = { viewModel.trySendAction(SetupUnlockAction.ContinueClick) },
|
||||
onSetUpLaterClick = { viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick) },
|
||||
)
|
||||
}
|
||||
}
|
58
app/src/main/res/drawable-night/account_setup.xml
Normal file
58
app/src/main/res/drawable-night/account_setup.xml
Normal file
|
@ -0,0 +1,58 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportHeight="200"
|
||||
android:viewportWidth="200">
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M21,16.62C21,9.65 26.65,4 33.62,4H93.31C100.29,4 105.94,9.65 105.94,16.62V41.26C105.94,42.36 105.04,43.26 103.94,43.26C102.83,43.26 101.94,42.36 101.94,41.26V16.62C101.94,11.86 98.08,8 93.31,8H33.62C28.86,8 25,11.86 25,16.62V146.52C25,151.29 28.86,155.14 33.62,155.14H93.59C94.7,155.14 95.59,156.04 95.59,157.14C95.59,158.25 94.7,159.14 93.59,159.14H33.62C26.65,159.14 21,153.49 21,146.52V16.62Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M59.42,22.21C59.42,21.11 60.31,20.21 61.42,20.21H65.51C66.61,20.21 67.51,21.11 67.51,22.21C67.51,23.32 66.61,24.21 65.51,24.21H61.42C60.31,24.21 59.42,23.32 59.42,22.21Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M105.7,44.86C100.94,44.86 97.08,48.71 97.08,53.48V183.38C97.08,188.14 100.94,192 105.7,192H165.38C170.14,192 174,188.14 174,183.38V53.48C174,48.71 170.14,44.86 165.38,44.86H105.7ZM93.08,53.48C93.08,46.5 98.73,40.86 105.7,40.86H165.38C172.35,40.86 178,46.5 178,53.48V183.38C178,190.35 172.35,196 165.38,196H105.7C98.73,196 93.08,190.35 93.08,183.38V53.48Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M130.14,59.07C130.14,57.97 131.04,57.07 132.14,57.07H136.41C137.51,57.07 138.41,57.97 138.41,59.07C138.41,60.18 137.51,61.07 136.41,61.07H132.14C131.04,61.07 130.14,60.18 130.14,59.07Z" />
|
||||
<path
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M77.46,58.89C77.46,57.79 78.35,56.89 79.46,56.89H83.24C85.55,56.89 87.6,58.71 87.6,61.14V64.73C87.6,65.84 86.7,66.73 85.6,66.73C84.49,66.73 83.6,65.84 83.6,64.73V61.14C83.6,61.11 83.59,61.06 83.52,61C83.46,60.94 83.36,60.89 83.24,60.89H79.46C78.35,60.89 77.46,60 77.46,58.89ZM36.42,61.15C36.42,58.71 38.45,56.91 40.77,56.91H44.55C45.66,56.91 46.55,57.8 46.55,58.91C46.55,60.01 45.66,60.91 44.55,60.91H40.77C40.63,60.91 40.54,60.96 40.48,61.01C40.43,61.06 40.42,61.11 40.42,61.15V64.75C40.42,65.85 39.52,66.75 38.42,66.75C37.31,66.75 36.42,65.85 36.42,64.75V61.15Z" />
|
||||
<path
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M38.42,103.91C39.52,103.91 40.42,104.8 40.42,105.91V109.49C40.42,109.51 40.43,109.57 40.49,109.63C40.55,109.69 40.65,109.73 40.77,109.73H44.55C45.66,109.73 46.55,110.63 46.55,111.73C46.55,112.84 45.66,113.73 44.55,113.73H40.77C38.46,113.73 36.42,111.91 36.42,109.49V105.91C36.42,104.8 37.31,103.91 38.42,103.91ZM85.6,103.91C86.7,103.91 87.6,104.8 87.6,105.91V109.49C87.6,111.9 85.57,113.73 83.24,113.73H79.46C78.35,113.73 77.46,112.84 77.46,111.73C77.46,110.63 78.35,109.73 79.46,109.73H83.24C83.37,109.73 83.47,109.68 83.53,109.63C83.58,109.57 83.6,109.52 83.6,109.49V105.91C83.6,104.8 84.49,103.91 85.6,103.91Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M62.15,70.01C57.23,70.01 53.38,72.96 53.38,76.96V79.51C53.38,79.51 53.38,79.52 53.38,79.52C53.38,79.62 53.38,80.05 53.18,80.51C53.08,80.75 52.95,80.93 52.87,81.02C52.82,81.09 52.77,81.15 52.74,81.18C52.71,81.21 52.68,81.25 52.65,81.28C52.64,81.3 52.62,81.32 52.61,81.33C52.55,81.4 52.48,81.48 52.4,81.58C52.37,81.62 52.36,81.64 52.35,81.66C52.34,81.67 52.33,81.7 52.33,81.76C52.33,81.78 52.32,81.8 52.32,81.82C52.31,81.94 52.32,82.21 52.53,82.79C53.3,84.95 54.78,87.61 56.59,89.73C58.44,91.88 60.41,93.21 62.15,93.21C63,93.21 63.97,92.89 64.97,92.26L64.97,92.25C65.98,91.62 66.98,90.72 67.93,89.64C69.69,87.61 71.03,85.14 71.52,83.17L71.09,80.86C71.05,80.79 71.01,80.73 70.98,80.66C70.88,80.46 70.81,80.25 70.78,80.03C70.78,80.02 70.78,80 70.77,79.98C70.76,79.93 70.76,79.87 70.75,79.8C70.75,79.73 70.74,79.66 70.74,79.58V78.87C70.74,78.77 70.75,78.68 70.76,78.59L70.7,76.76C70.43,74.49 69.48,72.84 68.07,71.74C66.63,70.62 64.62,70.01 62.15,70.01ZM70.77,79.78L70.77,79.78L70.77,79.78L70.77,79.79C70.77,79.79 70.77,79.78 70.77,79.78ZM75.47,83.81C75.53,83.64 75.56,83.46 75.57,83.28C75.69,82.52 75.73,81.77 75.62,81.07C75.53,80.45 75.34,79.94 75.1,79.53L74.69,76.38C74.32,73.13 72.9,70.43 70.53,68.59L70.53,68.59C68.2,66.78 65.24,66.01 62.15,66.01C56.06,66.01 49.38,69.84 49.38,76.96V78.96C49.35,78.99 49.32,79.02 49.3,79.05L49.3,79.06C48.85,79.61 48.43,80.39 48.34,81.42C48.24,82.39 48.47,83.31 48.76,84.13L48.76,84.13C49.69,86.75 51.42,89.84 53.55,92.33L53.55,92.33C55.56,94.67 58.56,97.21 62.15,97.21C63.98,97.21 65.68,96.54 67.1,95.64C68.53,94.75 69.82,93.54 70.94,92.28L70.94,92.27C73.08,89.81 74.86,86.65 75.47,83.81ZM49.38,79.52C49.38,79.52 49.38,79.52 49.38,79.52L49.38,79.52Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M56.68,91.31C57.92,91.17 59.05,92.07 59.18,93.32C59.29,94.28 59.33,95.8 59.14,97.39C58.96,98.93 58.52,100.84 57.44,102.34C56.71,103.36 55.29,103.59 54.28,102.86C53.26,102.13 53.02,100.71 53.76,99.69C54.16,99.12 54.48,98.12 54.63,96.85C54.78,95.63 54.74,94.46 54.67,93.81C54.53,92.56 55.43,91.44 56.68,91.31Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M68.12,91.31C69.37,91.44 70.27,92.56 70.13,93.81C70.06,94.46 70.02,95.63 70.17,96.85C70.32,98.12 70.64,99.12 71.04,99.69C71.78,100.71 71.54,102.13 70.52,102.86C69.51,103.59 68.09,103.36 67.36,102.34C66.28,100.84 65.84,98.93 65.66,97.39C65.47,95.8 65.51,94.28 65.62,93.32C65.75,92.07 66.88,91.17 68.12,91.31Z" />
|
||||
<path
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M104.11,118.56C104.11,110.43 110.72,103.86 118.83,103.86H152.25C160.39,103.86 166.98,110.45 166.98,118.56C166.98,126.68 160.37,133.25 152.25,133.25H118.83C110.7,133.25 104.11,126.66 104.11,118.56ZM118.83,107.86C112.91,107.86 108.11,112.65 108.11,118.56C108.11,124.45 112.9,129.25 118.83,129.25H152.25C158.18,129.25 162.98,124.46 162.98,118.56C162.98,112.67 158.19,107.86 152.25,107.86H118.83Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:pathData="M119.37,122.22C121.4,122.22 123.04,120.58 123.04,118.55C123.04,116.53 121.4,114.89 119.37,114.89C117.33,114.89 115.69,116.53 115.69,118.55C115.69,120.58 117.33,122.22 119.37,122.22Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:pathData="M129.66,122.22C131.7,122.22 133.34,120.58 133.34,118.55C133.34,116.53 131.7,114.89 129.66,114.89C127.63,114.89 125.99,116.53 125.99,118.55C125.99,120.58 127.63,122.22 129.66,122.22Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:pathData="M139.98,122.22C142.01,122.22 143.66,120.58 143.66,118.55C143.66,116.53 142.01,114.89 139.98,114.89C137.95,114.89 136.3,116.53 136.3,118.55C136.3,120.58 137.95,122.22 139.98,122.22Z" />
|
||||
<path
|
||||
android:fillColor="#E2E3E4"
|
||||
android:pathData="M150.27,122.22C152.3,122.22 153.95,120.58 153.95,118.55C153.95,116.53 152.3,114.89 150.27,114.89C148.24,114.89 146.59,116.53 146.59,118.55C146.59,120.58 148.24,122.22 150.27,122.22Z" />
|
||||
</vector>
|
58
app/src/main/res/drawable/account_setup.xml
Normal file
58
app/src/main/res/drawable/account_setup.xml
Normal file
|
@ -0,0 +1,58 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportHeight="200"
|
||||
android:viewportWidth="200">
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M21,16.62C21,9.65 26.65,4 33.62,4H93.31C100.29,4 105.94,9.65 105.94,16.62V41.26C105.94,42.36 105.04,43.26 103.94,43.26C102.83,43.26 101.94,42.36 101.94,41.26V16.62C101.94,11.86 98.08,8 93.31,8H33.62C28.86,8 25,11.86 25,16.62V146.52C25,151.29 28.86,155.14 33.62,155.14H93.59C94.7,155.14 95.59,156.04 95.59,157.14C95.59,158.25 94.7,159.14 93.59,159.14H33.62C26.65,159.14 21,153.49 21,146.52V16.62Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M59.42,22.21C59.42,21.11 60.31,20.21 61.42,20.21H65.51C66.61,20.21 67.51,21.11 67.51,22.21C67.51,23.32 66.61,24.21 65.51,24.21H61.42C60.31,24.21 59.42,23.32 59.42,22.21Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M105.7,44.86C100.94,44.86 97.08,48.71 97.08,53.48V183.38C97.08,188.14 100.94,192 105.7,192H165.38C170.14,192 174,188.14 174,183.38V53.48C174,48.71 170.14,44.86 165.38,44.86H105.7ZM93.08,53.48C93.08,46.5 98.73,40.86 105.7,40.86H165.38C172.35,40.86 178,46.5 178,53.48V183.38C178,190.35 172.35,196 165.38,196H105.7C98.73,196 93.08,190.35 93.08,183.38V53.48Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M130.14,59.07C130.14,57.97 131.04,57.07 132.14,57.07H136.41C137.51,57.07 138.41,57.97 138.41,59.07C138.41,60.18 137.51,61.07 136.41,61.07H132.14C131.04,61.07 130.14,60.18 130.14,59.07Z" />
|
||||
<path
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M77.46,58.89C77.46,57.79 78.35,56.89 79.46,56.89H83.24C85.55,56.89 87.6,58.71 87.6,61.14V64.73C87.6,65.84 86.7,66.73 85.6,66.73C84.49,66.73 83.6,65.84 83.6,64.73V61.14C83.6,61.11 83.59,61.06 83.52,61C83.46,60.94 83.36,60.89 83.24,60.89H79.46C78.35,60.89 77.46,60 77.46,58.89ZM36.42,61.15C36.42,58.71 38.45,56.91 40.77,56.91H44.55C45.66,56.91 46.55,57.8 46.55,58.91C46.55,60.01 45.66,60.91 44.55,60.91H40.77C40.63,60.91 40.54,60.96 40.48,61.01C40.43,61.06 40.42,61.11 40.42,61.15V64.75C40.42,65.85 39.52,66.75 38.42,66.75C37.31,66.75 36.42,65.85 36.42,64.75V61.15Z" />
|
||||
<path
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M38.42,103.91C39.52,103.91 40.42,104.8 40.42,105.91V109.49C40.42,109.51 40.43,109.57 40.49,109.63C40.55,109.69 40.65,109.73 40.77,109.73H44.55C45.66,109.73 46.55,110.63 46.55,111.73C46.55,112.84 45.66,113.73 44.55,113.73H40.77C38.46,113.73 36.42,111.91 36.42,109.49V105.91C36.42,104.8 37.31,103.91 38.42,103.91ZM85.6,103.91C86.7,103.91 87.6,104.8 87.6,105.91V109.49C87.6,111.9 85.57,113.73 83.24,113.73H79.46C78.35,113.73 77.46,112.84 77.46,111.73C77.46,110.63 78.35,109.73 79.46,109.73H83.24C83.37,109.73 83.47,109.68 83.53,109.63C83.58,109.57 83.6,109.52 83.6,109.49V105.91C83.6,104.8 84.49,103.91 85.6,103.91Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M62.15,70.01C57.23,70.01 53.38,72.96 53.38,76.96V79.51C53.38,79.51 53.38,79.52 53.38,79.52C53.38,79.62 53.38,80.05 53.18,80.51C53.08,80.75 52.95,80.93 52.87,81.02C52.82,81.09 52.77,81.15 52.74,81.18C52.71,81.21 52.68,81.25 52.65,81.28C52.64,81.3 52.62,81.32 52.61,81.33C52.55,81.4 52.48,81.48 52.4,81.58C52.37,81.62 52.36,81.64 52.35,81.66C52.34,81.67 52.33,81.7 52.33,81.76C52.33,81.78 52.32,81.8 52.32,81.82C52.31,81.94 52.32,82.21 52.53,82.79C53.3,84.95 54.78,87.61 56.59,89.73C58.44,91.88 60.41,93.21 62.15,93.21C63,93.21 63.97,92.89 64.97,92.26L64.97,92.25C65.98,91.62 66.98,90.72 67.93,89.64C69.69,87.61 71.03,85.14 71.52,83.17L71.09,80.86C71.05,80.79 71.01,80.73 70.98,80.66C70.88,80.46 70.81,80.25 70.78,80.03C70.78,80.02 70.78,80 70.77,79.98C70.76,79.93 70.76,79.87 70.75,79.8C70.75,79.73 70.74,79.66 70.74,79.58V78.87C70.74,78.77 70.75,78.68 70.76,78.59L70.7,76.76C70.43,74.49 69.48,72.84 68.07,71.74C66.63,70.62 64.62,70.01 62.15,70.01ZM70.77,79.78L70.77,79.78L70.77,79.78L70.77,79.79C70.77,79.79 70.77,79.78 70.77,79.78ZM75.47,83.81C75.53,83.64 75.56,83.46 75.57,83.28C75.69,82.52 75.73,81.77 75.62,81.07C75.53,80.45 75.34,79.94 75.1,79.53L74.69,76.38C74.32,73.13 72.9,70.43 70.53,68.59L70.53,68.59C68.2,66.78 65.24,66.01 62.15,66.01C56.06,66.01 49.38,69.84 49.38,76.96V78.96C49.35,78.99 49.32,79.02 49.3,79.05L49.3,79.06C48.85,79.61 48.43,80.39 48.34,81.42C48.24,82.39 48.47,83.31 48.76,84.13L48.76,84.13C49.69,86.75 51.42,89.84 53.55,92.33L53.55,92.33C55.56,94.67 58.56,97.21 62.15,97.21C63.98,97.21 65.68,96.54 67.1,95.64C68.53,94.75 69.82,93.54 70.94,92.28L70.94,92.27C73.08,89.81 74.86,86.65 75.47,83.81ZM49.38,79.52C49.38,79.52 49.38,79.52 49.38,79.52L49.38,79.52Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M56.68,91.31C57.92,91.17 59.05,92.07 59.18,93.32C59.29,94.28 59.33,95.8 59.14,97.39C58.96,98.93 58.52,100.84 57.44,102.34C56.71,103.36 55.29,103.59 54.28,102.86C53.26,102.13 53.02,100.71 53.76,99.69C54.16,99.12 54.48,98.12 54.63,96.85C54.78,95.64 54.74,94.46 54.67,93.81C54.53,92.57 55.43,91.44 56.68,91.31Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M68.12,91.31C69.37,91.44 70.27,92.57 70.13,93.81C70.06,94.46 70.02,95.64 70.17,96.85C70.32,98.12 70.64,99.12 71.04,99.69C71.78,100.71 71.54,102.13 70.52,102.86C69.51,103.59 68.09,103.36 67.36,102.34C66.28,100.84 65.84,98.93 65.66,97.39C65.47,95.8 65.51,94.28 65.62,93.32C65.75,92.07 66.88,91.17 68.12,91.31Z" />
|
||||
<path
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M104.11,118.55C104.11,110.43 110.72,103.86 118.83,103.86H152.25C160.39,103.86 166.98,110.45 166.98,118.55C166.98,126.68 160.37,133.25 152.25,133.25H118.83C110.7,133.25 104.11,126.66 104.11,118.55ZM118.83,107.86C112.91,107.86 108.11,112.65 108.11,118.55C108.11,124.44 112.9,129.25 118.83,129.25H152.25C158.18,129.25 162.98,124.46 162.98,118.55C162.98,112.66 158.19,107.86 152.25,107.86H118.83Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M119.37,122.22C121.4,122.22 123.04,120.58 123.04,118.55C123.04,116.53 121.4,114.89 119.37,114.89C117.33,114.89 115.69,116.53 115.69,118.55C115.69,120.58 117.33,122.22 119.37,122.22Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M129.66,122.22C131.7,122.22 133.34,120.58 133.34,118.55C133.34,116.53 131.7,114.89 129.66,114.89C127.63,114.89 125.99,116.53 125.99,118.55C125.99,120.58 127.63,122.22 129.66,122.22Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M139.98,122.22C142.01,122.22 143.66,120.58 143.66,118.55C143.66,116.53 142.01,114.89 139.98,114.89C137.95,114.89 136.3,116.53 136.3,118.55C136.3,120.58 137.95,122.22 139.98,122.22Z" />
|
||||
<path
|
||||
android:fillColor="#020F66"
|
||||
android:pathData="M150.27,122.22C152.3,122.22 153.95,120.58 153.95,118.55C153.95,116.53 152.3,114.89 150.27,114.89C148.24,114.89 146.59,116.53 146.59,118.55C146.59,120.58 148.24,122.22 150.27,122.22Z" />
|
||||
</vector>
|
|
@ -945,4 +945,11 @@ Do you want to switch to this account?</string>
|
|||
<string name="totally_different_from_your_other_passwords">Totally different from your other passwords</string>
|
||||
<string name="use_the_generator_to_create_a_strong_unique_password">Use the generator to create a strong, unique password</string>
|
||||
<string name="try_it_out">Try it out</string>
|
||||
<string name="account_setup">Account setup</string>
|
||||
<string name="set_up_unlock">Set up unlock</string>
|
||||
<string name="set_up_later">Set up later</string>
|
||||
<string name="set_up_unlock_later">Set up unlock later?</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>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,478 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsOff
|
||||
import androidx.compose.ui.test.assertIsOn
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
import androidx.compose.ui.test.isDialog
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.components.toggle.UnlockWithPinState
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.util.assertNoDialogExists
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
class SetupUnlockScreenTest : BaseComposeTest() {
|
||||
|
||||
private var onNavigateToSetupAutofillCalled = false
|
||||
|
||||
private val biometricsManager: BiometricsManager = mockk {
|
||||
every { isBiometricsSupported } returns true
|
||||
}
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<SetupUnlockEvent>()
|
||||
private val viewModel = mockk<SetupUnlockViewModel> {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
every { trySendAction(action = any()) } just runs
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
composeTestRule.setContent {
|
||||
SetupUnlockScreen(
|
||||
onNavigateToSetupAutofill = { onNavigateToSetupAutofillCalled = true },
|
||||
viewModel = viewModel,
|
||||
biometricsManager = biometricsManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Config(qualifiers = "land")
|
||||
@Test
|
||||
fun `header should display in landscape mode`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Set up unlock")
|
||||
.performScrollTo()
|
||||
.assertExists()
|
||||
.assertIsDisplayed()
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
composeTestRule
|
||||
.onNodeWithText(
|
||||
text = "Set up biometrics or choose a PIN code to quickly access your vault and AutoFill your logins.",
|
||||
)
|
||||
.performScrollTo()
|
||||
.assertExists()
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToSetupAutofill event should invoke the navigate to autofill lambda`() {
|
||||
mutableEventFlow.tryEmit(SetupUnlockEvent.NavigateToSetupAutofill)
|
||||
assertTrue(onNavigateToSetupAutofillCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with biometrics should be toggled on or off according to state`() {
|
||||
composeTestRule.onNodeWithText(text = "Unlock with Biometrics").assertIsOff()
|
||||
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = true) }
|
||||
composeTestRule.onNodeWithText(text = "Unlock with Biometrics").assertIsOn()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on unlock with biometrics toggle should send EnableBiometricsClick when isUnlockWithBiometricsEnabled is false`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with Biometrics")
|
||||
.performScrollTo()
|
||||
.assertIsOff()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(SetupUnlockAction.EnableBiometricsClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with biometrics toggle should send UnlockWithBiometricToggle`() {
|
||||
mutableStateFlow.update { it.copy(isUnlockWithBiometricsEnabled = true) }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with Biometrics")
|
||||
.performScrollTo()
|
||||
.assertIsOn()
|
||||
.performClick()
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(SetupUnlockAction.UnlockWithBiometricToggle(isEnabled = false))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with pin code should be toggled on or off according to state`() {
|
||||
composeTestRule.onNodeWithText(text = "Unlock with PIN code").assertIsOff()
|
||||
mutableStateFlow.update { it.copy(isUnlockWithPinEnabled = true) }
|
||||
composeTestRule.onNodeWithText(text = "Unlock with PIN code").assertIsOn()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on unlock with pin toggle when enabled should send UnlockWithPinToggle Disabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = true)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(UnlockWithPinState.Disabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on unlock with pin toggle when disabled should show the PIN input dialog and send UnlockWithPinToggle PendingEnabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = false)
|
||||
}
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Enter your PIN code.")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "Set your PIN code for unlocking Bitwarden. Your PIN settings will be reset if " +
|
||||
"you ever fully log out of the application.",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(UnlockWithPinState.PendingEnabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PIN input dialog Cancel click should clear the dialog and send UnlockWithPinToggle Disabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = false)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(UnlockWithPinState.Disabled),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PIN input dialog Submit click with empty pin should clear the dialog and send UnlockWithPinToggle Disabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = false)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(UnlockWithPinState.Disabled),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PIN input dialog Submit click with non-empty pin and isUnlockWithPasswordEnabled true should show a confirmation dialog and send UnlockWithPinToggle PendingEnabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = false)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput(text = "1234")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Unlock with PIN code")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(
|
||||
text = "Do you want to require unlocking with your master password when the application " +
|
||||
"is restarted?",
|
||||
)
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Yes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "No")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(UnlockWithPinState.PendingEnabled),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PIN input dialog Submit click with non-empty pin and isUnlockWithPasswordEnabled false should show a confirmation dialog and send UnlockWithPinToggle Enabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isUnlockWithPinEnabled = false,
|
||||
isUnlockWithPasswordEnabled = false,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput(text = "1234")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(
|
||||
UnlockWithPinState.Enabled(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PIN confirmation dialog No click should send UnlockWithPinToggle Enabled and close the dialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = false)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput(text = "1234")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "No")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(
|
||||
UnlockWithPinState.Enabled(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `PIN confirmation dialog Yes click should send UnlockWithPinToggle Enabled and close the dialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = false)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Unlock with PIN code")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "PIN")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performTextInput(text = "1234")
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Submit")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Yes")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
SetupUnlockAction.UnlockWithPinToggle(
|
||||
UnlockWithPinState.Enabled(
|
||||
pin = "1234",
|
||||
shouldRequireMasterPasswordOnRestart = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Continue click should send ContinueClick when disabled`() {
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Continue")
|
||||
.performScrollTo()
|
||||
.assertIsDisplayed()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 0) {
|
||||
viewModel.trySendAction(SetupUnlockAction.ContinueClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Continue click should send ContinueClick when enabled`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isUnlockWithPinEnabled = true)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Continue")
|
||||
.performScrollTo()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(SetupUnlockAction.ContinueClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Set up later click should display confirmation dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Set up later")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Set up unlock later?")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on Set up later dialog cancel click should dismiss the dialog`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Set up later")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Cancel")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `on Set up later dialog confirm click should dismiss the dialog and send SetUpLaterClick`() {
|
||||
composeTestRule.assertNoDialogExists()
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Set up later")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "Confirm")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
composeTestRule.assertNoDialogExists()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: SetupUnlockState = SetupUnlockState(
|
||||
isUnlockWithPinEnabled = false,
|
||||
isUnlockWithPasswordEnabled = true,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
)
|
|
@ -0,0 +1,98 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.accountsetup
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.BiometricsEncryptionManager
|
||||
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import javax.crypto.Cipher
|
||||
|
||||
class SetupUnlockViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableUserStateFlow = MutableStateFlow<UserState?>(DEFAULT_USER_STATE)
|
||||
private val authRepository: AuthRepository = mockk {
|
||||
every { userStateFlow } returns mutableUserStateFlow
|
||||
}
|
||||
private val settingsRepository = mockk<SettingsRepository> {
|
||||
every { isUnlockWithPinEnabled } returns false
|
||||
every { isUnlockWithBiometricsEnabled } returns false
|
||||
}
|
||||
private val biometricsEncryptionManager: BiometricsEncryptionManager = mockk {
|
||||
every { getOrCreateCipher(userId = DEFAULT_USER_ID) } returns CIPHER
|
||||
every {
|
||||
isBiometricIntegrityValid(userId = DEFAULT_USER_ID, cipher = CIPHER)
|
||||
} returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ContinueClick should emit NavigateToSetupAutofill`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SetupUnlockAction.ContinueClick)
|
||||
assertEquals(SetupUnlockEvent.NavigateToSetupAutofill, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SetUpLaterClick should emit NavigateToSetupAutofill`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(SetupUnlockAction.SetUpLaterClick)
|
||||
assertEquals(SetupUnlockEvent.NavigateToSetupAutofill, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(
|
||||
state: SetupUnlockState? = null,
|
||||
): SetupUnlockViewModel =
|
||||
SetupUnlockViewModel(
|
||||
savedStateHandle = SavedStateHandle(mapOf("state" to state)),
|
||||
authRepository = authRepository,
|
||||
settingsRepository = settingsRepository,
|
||||
biometricsEncryptionManager = biometricsEncryptionManager,
|
||||
)
|
||||
}
|
||||
|
||||
private val DEFAULT_STATE: SetupUnlockState = SetupUnlockState(
|
||||
isUnlockWithPinEnabled = false,
|
||||
isUnlockWithPasswordEnabled = true,
|
||||
isUnlockWithBiometricsEnabled = false,
|
||||
)
|
||||
|
||||
private val CIPHER = mockk<Cipher>()
|
||||
private const val DEFAULT_USER_ID: String = "activeUserId"
|
||||
private val DEFAULT_USER_STATE: UserState = UserState(
|
||||
activeUserId = DEFAULT_USER_ID,
|
||||
accounts = listOf(
|
||||
UserState.Account(
|
||||
userId = DEFAULT_USER_ID,
|
||||
name = "Active User",
|
||||
email = "active@bitwarden.com",
|
||||
avatarColorHex = "#aa00aa",
|
||||
environment = Environment.Us,
|
||||
isPremium = true,
|
||||
isLoggedIn = true,
|
||||
isVaultUnlocked = true,
|
||||
needsPasswordReset = false,
|
||||
isBiometricsEnabled = false,
|
||||
organizations = emptyList(),
|
||||
needsMasterPassword = false,
|
||||
trustedDevice = null,
|
||||
),
|
||||
),
|
||||
)
|
Loading…
Add table
Reference in a new issue