[PM-26810] Add OTP support to VerifyPasswordScreen (#6034)

This commit is contained in:
Patrick Honkonen 2025-10-16 17:02:52 -04:00 committed by GitHub
parent ae3470c598
commit 74aa0a78ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 915 additions and 439 deletions

View File

@ -5,6 +5,7 @@ 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.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
@ -15,8 +16,10 @@ import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -25,14 +28,18 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitwarden.ui.platform.base.util.EventsEffect
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
import com.bitwarden.ui.platform.components.button.BitwardenOutlinedButton
import com.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.bitwarden.ui.platform.components.field.BitwardenPasswordField
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarHostState
import com.bitwarden.ui.platform.components.snackbar.model.rememberBitwardenSnackbarHostState
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.bitwarden.ui.platform.resource.BitwardenDrawable
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.vault.feature.exportitems.component.AccountSummaryListItem
import com.x8bit.bitwarden.ui.vault.feature.exportitems.component.ExportItemsScaffold
import com.x8bit.bitwarden.ui.vault.feature.exportitems.model.AccountSelectionListItem
@ -47,6 +54,7 @@ fun VerifyPasswordScreen(
onNavigateBack: () -> Unit,
onPasswordVerified: (userId: String) -> Unit,
viewModel: VerifyPasswordViewModel = hiltViewModel(),
snackbarHostState: BitwardenSnackbarHostState = rememberBitwardenSnackbarHostState(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@ -59,6 +67,10 @@ fun VerifyPasswordScreen(
is VerifyPasswordEvent.PasswordVerified -> {
onPasswordVerified(event.userId)
}
is VerifyPasswordEvent.ShowSnackbar -> {
snackbarHostState.showSnackbar(event.data)
}
}
}
@ -79,10 +91,9 @@ fun VerifyPasswordScreen(
VerifyPasswordContent(
state = state,
onInputChanged = handler.onInputChanged,
onUnlockClick = handler.onUnlockClick,
modifier = Modifier
.fillMaxSize()
.standardHorizontalMargin(),
onContinueClick = handler.onContinueClick,
onResendCodeClick = handler.onSendCodeClick,
modifier = Modifier.fillMaxSize(),
)
}
}
@ -110,11 +121,13 @@ private fun VerifyPasswordDialogs(
}
}
@Suppress("LongMethod")
@Composable
private fun VerifyPasswordContent(
state: VerifyPasswordState,
onInputChanged: (String) -> Unit,
onUnlockClick: () -> Unit,
onContinueClick: () -> Unit,
onResendCodeClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
@ -125,61 +138,121 @@ private fun VerifyPasswordContent(
Spacer(Modifier.height(24.dp))
Text(
text = stringResource(BitwardenString.verify_your_master_password),
text = state.title(),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.titleMedium,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
state.subtext?.let { subtext ->
Spacer(Modifier.height(8.dp))
Text(
text = subtext(),
textAlign = TextAlign.Center,
style = BitwardenTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
Spacer(Modifier.height(16.dp))
AccountSummaryListItem(
item = state.accountSummaryListItem,
cardStyle = CardStyle.Full,
clickable = false,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
Spacer(Modifier.height(16.dp))
BitwardenPasswordField(
label = stringResource(BitwardenString.master_password),
value = state.input,
onValueChange = onInputChanged,
showPasswordTestTag = "PasswordVisibilityToggle",
imeAction = ImeAction.Done,
keyboardActions = KeyboardActions(
onDone = {
if (state.isUnlockButtonEnabled) {
onUnlockClick()
} else {
defaultKeyboardAction(ImeAction.Done)
}
},
),
supportingText = stringResource(BitwardenString.vault_locked_master_password),
passwordFieldTestTag = "MasterPasswordEntry",
cardStyle = CardStyle.Full,
modifier = Modifier.fillMaxWidth(),
)
if (state.showResendCodeButton) {
BitwardenPasswordField(
label = stringResource(id = BitwardenString.verification_code),
value = state.input,
onValueChange = onInputChanged,
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done,
keyboardActions = KeyboardActions(
onDone = {
if (state.isContinueButtonEnabled) {
onContinueClick()
} else {
defaultKeyboardAction(ImeAction.Done)
}
},
),
autoFocus = true,
cardStyle = CardStyle.Full,
passwordFieldTestTag = "VerificationCodeEntry",
showPasswordTestTag = "VerificationCodeVisibilityToggle",
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
} else {
BitwardenPasswordField(
label = stringResource(BitwardenString.master_password),
value = state.input,
onValueChange = onInputChanged,
showPasswordTestTag = "PasswordVisibilityToggle",
imeAction = ImeAction.Done,
keyboardActions = KeyboardActions(
onDone = {
if (state.isContinueButtonEnabled) {
onContinueClick()
} else {
defaultKeyboardAction(ImeAction.Done)
}
},
),
autoFocus = true,
supportingText = stringResource(BitwardenString.vault_locked_master_password),
passwordFieldTestTag = "MasterPasswordEntry",
cardStyle = CardStyle.Full,
modifier = Modifier
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
Spacer(Modifier.height(16.dp))
BitwardenFilledButton(
label = stringResource(BitwardenString.unlock),
onClick = onUnlockClick,
isEnabled = state.isUnlockButtonEnabled,
modifier = Modifier.fillMaxWidth(),
label = stringResource(BitwardenString.continue_text),
onClick = onContinueClick,
isEnabled = state.isContinueButtonEnabled,
modifier = Modifier
.testTag("ContinueImportButton")
.fillMaxWidth()
.standardHorizontalMargin(),
)
if (state.showResendCodeButton) {
BitwardenOutlinedButton(
label = stringResource(BitwardenString.resend_code),
onClick = onResendCodeClick,
modifier = Modifier
.testTag("ResendTOTPCodeButton")
.fillMaxWidth()
.standardHorizontalMargin(),
)
}
Spacer(Modifier.height(12.dp))
Spacer(Modifier.navigationBarsPadding())
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
private fun VerifyPasswordContent_Preview() {
private fun VerifyPasswordContent_MasterPassword_preview() {
val accountSummaryListItem = AccountSelectionListItem(
userId = "userId",
isItemRestricted = false,
@ -188,14 +261,65 @@ private fun VerifyPasswordContent_Preview() {
email = "john.doe@example.com",
)
val state = VerifyPasswordState(
title = BitwardenString.verify_your_master_password.asText(),
subtext = null,
accountSummaryListItem = accountSummaryListItem,
)
VerifyPasswordContent(
state = state,
onInputChanged = {},
onUnlockClick = {},
modifier = Modifier
.fillMaxSize()
.standardHorizontalMargin(),
)
ExportItemsScaffold(
navIcon = rememberVectorPainter(
BitwardenDrawable.ic_back,
),
onNavigationIconClick = {},
navigationIconContentDescription = stringResource(BitwardenString.back),
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
modifier = Modifier.fillMaxSize(),
) {
VerifyPasswordContent(
state = state,
onInputChanged = {},
onContinueClick = {},
onResendCodeClick = {},
modifier = Modifier
.fillMaxSize(),
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
private fun VerifyPasswordContent_Otp_preview() {
val accountSummaryListItem = AccountSelectionListItem(
userId = "userId",
isItemRestricted = false,
avatarColorHex = "#FF0000",
initials = "JD",
email = "john.doe@example.com",
)
val state = VerifyPasswordState(
title = BitwardenString.verify_your_account_email_address.asText(),
subtext = BitwardenString
.enter_the_6_digit_code_that_was_emailed_to_the_address_below
.asText(),
accountSummaryListItem = accountSummaryListItem,
showResendCodeButton = true,
)
ExportItemsScaffold(
navIcon = rememberVectorPainter(
BitwardenDrawable.ic_back,
),
onNavigationIconClick = {},
navigationIconContentDescription = stringResource(BitwardenString.back),
scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()),
modifier = Modifier.fillMaxSize(),
) {
VerifyPasswordContent(
state = state,
onInputChanged = {},
onContinueClick = {},
onResendCodeClick = {},
modifier = Modifier
.fillMaxSize(),
)
}
}

View File

@ -5,12 +5,15 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.bitwarden.network.model.PolicyTypeJson
import com.bitwarden.ui.platform.base.BaseViewModel
import com.bitwarden.ui.platform.components.snackbar.model.BitwardenSnackbarData
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.RequestOtpResult
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
import com.x8bit.bitwarden.data.auth.repository.model.ValidatePasswordResult
import com.x8bit.bitwarden.data.auth.repository.model.VerifyOtpResult
import com.x8bit.bitwarden.data.platform.manager.PolicyManager
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
@ -59,6 +62,15 @@ class VerifyPasswordViewModel @Inject constructor(
.map { it.organizationId }
VerifyPasswordState(
title = if (account.hasMasterPassword) {
BitwardenString.verify_your_master_password.asText()
} else {
BitwardenString.verify_your_account_email_address.asText()
},
subtext = BitwardenString
.enter_the_6_digit_code_that_was_emailed_to_the_address_below
.asText()
.takeUnless { account.hasMasterPassword },
accountSummaryListItem = AccountSelectionListItem(
userId = args.userId,
avatarColorHex = account.avatarColorHex,
@ -68,6 +80,7 @@ class VerifyPasswordViewModel @Inject constructor(
.organizations
.any { it.id in restrictedItemPolicyOrgIds },
),
showResendCodeButton = !account.hasMasterPassword,
)
},
) {
@ -77,6 +90,16 @@ class VerifyPasswordViewModel @Inject constructor(
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
if (stateFlow.value.showResendCodeButton) {
viewModelScope.launch {
sendAction(
VerifyPasswordAction.Internal.SendOtpCodeResultReceive(
result = authRepository.requestOneTimePasscode(),
),
)
}
}
}
override fun onCleared() {
@ -92,8 +115,8 @@ class VerifyPasswordViewModel @Inject constructor(
handleNavigateBackClick()
}
VerifyPasswordAction.UnlockClick -> {
handleUnlockClick()
VerifyPasswordAction.ContinueClick -> {
handleContinueClick()
}
is VerifyPasswordAction.PasswordInputChangeReceive -> {
@ -104,6 +127,10 @@ class VerifyPasswordViewModel @Inject constructor(
handleDismissDialog()
}
VerifyPasswordAction.ResendCodeClick -> {
handleResendCodeClick()
}
is VerifyPasswordAction.Internal -> {
handleInternalAction(action)
}
@ -114,7 +141,7 @@ class VerifyPasswordViewModel @Inject constructor(
sendEvent(VerifyPasswordEvent.NavigateBack)
}
private fun handleUnlockClick() {
private fun handleContinueClick() {
if (state.input.isBlank()) {
mutableStateFlow.update {
it.copy(
@ -154,6 +181,23 @@ class VerifyPasswordViewModel @Inject constructor(
mutableStateFlow.update { it.copy(dialog = null) }
}
private fun handleResendCodeClick() {
mutableStateFlow.update {
it.copy(
dialog = VerifyPasswordState.DialogState.Loading(
message = BitwardenString.sending.asText(),
),
)
}
viewModelScope.launch {
sendAction(
VerifyPasswordAction.Internal.SendOtpCodeResultReceive(
result = authRepository.requestOneTimePasscode(),
),
)
}
}
private fun handleInternalAction(action: VerifyPasswordAction.Internal) {
when (action) {
is VerifyPasswordAction.Internal.ValidatePasswordResultReceive -> {
@ -163,6 +207,14 @@ class VerifyPasswordViewModel @Inject constructor(
is VerifyPasswordAction.Internal.UnlockVaultResultReceive -> {
handleUnlockVaultResultReceive(action)
}
is VerifyPasswordAction.Internal.SendOtpCodeResultReceive -> {
handleSendOtpCodeResultReceive(action)
}
is VerifyPasswordAction.Internal.VerifyOtpResultReceive -> {
handleVerifyOtpResultReceive(action)
}
}
}
@ -221,6 +273,60 @@ class VerifyPasswordViewModel @Inject constructor(
}
}
private fun handleSendOtpCodeResultReceive(
action: VerifyPasswordAction.Internal.SendOtpCodeResultReceive,
) {
when (val result = action.result) {
is RequestOtpResult.Error -> {
val message = result.message?.asText()
?: BitwardenString.generic_error_message.asText()
mutableStateFlow.update {
it.copy(
dialog = VerifyPasswordState.DialogState.General(
title = BitwardenString.an_error_has_occurred.asText(),
message = message,
),
)
}
}
RequestOtpResult.Success -> {
mutableStateFlow.update {
it.copy(dialog = null)
}
sendEvent(
VerifyPasswordEvent.ShowSnackbar(BitwardenString.code_sent.asText()),
)
}
}
}
private fun handleVerifyOtpResultReceive(
action: VerifyPasswordAction.Internal.VerifyOtpResultReceive,
) {
when (action.result) {
is VerifyOtpResult.Verified -> {
mutableStateFlow.update { it.copy(dialog = null) }
sendEvent(
VerifyPasswordEvent.PasswordVerified(
state.accountSummaryListItem.userId,
),
)
}
is VerifyOtpResult.NotVerified -> {
mutableStateFlow.update {
it.copy(
dialog = VerifyPasswordState.DialogState.General(
title = BitwardenString.an_error_has_occurred.asText(),
message = BitwardenString.invalid_verification_code.asText(),
),
)
}
}
}
}
private fun switchAccountAndVerifyPassword() {
val switchAccountResult = authRepository
.switchAccount(userId = state.accountSummaryListItem.userId)
@ -244,7 +350,15 @@ class VerifyPasswordViewModel @Inject constructor(
val userId = state.accountSummaryListItem.userId
viewModelScope.launch {
if (vaultRepository.isVaultUnlocked(userId)) {
if (state.showResendCodeButton) {
sendAction(
VerifyPasswordAction.Internal.VerifyOtpResultReceive(
result = authRepository.verifyOneTimePasscode(
oneTimePasscode = state.input,
),
),
)
} else if (vaultRepository.isVaultUnlocked(userId)) {
// If the vault is already unlocked, validate the password directly.
sendAction(
VerifyPasswordAction.Internal.ValidatePasswordResultReceive(
@ -300,19 +414,23 @@ class VerifyPasswordViewModel @Inject constructor(
* @param accountSummaryListItem The account summary to display.
* @param input The current password input.
* @param dialog The current dialog state, or null if no dialog is shown.
* @param showResendCodeButton Whether to show the send code button.
*/
@Parcelize
data class VerifyPasswordState(
val accountSummaryListItem: AccountSelectionListItem,
val title: Text,
val subtext: Text?,
// We never want this saved since the input is sensitive data.
@IgnoredOnParcel val input: String = "",
val dialog: DialogState? = null,
val showResendCodeButton: Boolean = false,
) : Parcelable {
/**
* Whether the unlock button should be enabled.
*/
val isUnlockButtonEnabled: Boolean
val isContinueButtonEnabled: Boolean
get() = input.isNotBlank() && dialog !is DialogState.Loading
/**
@ -356,6 +474,27 @@ sealed class VerifyPasswordEvent {
* @param userId The ID of the user whose password was verified.
*/
data class PasswordVerified(val userId: String) : VerifyPasswordEvent()
/**
* Show a snackbar with the given data.
*/
data class ShowSnackbar(
val data: BitwardenSnackbarData,
) : VerifyPasswordEvent() {
constructor(
message: Text,
messageHeader: Text? = null,
actionLabel: Text? = null,
withDismissAction: Boolean = false,
) : this(
data = BitwardenSnackbarData(
message = message,
messageHeader = messageHeader,
actionLabel = actionLabel,
withDismissAction = withDismissAction,
),
)
}
}
/**
@ -368,15 +507,20 @@ sealed class VerifyPasswordAction {
data object NavigateBackClick : VerifyPasswordAction()
/**
* Represents a click on the unlock button.
* Represents a click on the Continue button.
*/
data object UnlockClick : VerifyPasswordAction()
data object ContinueClick : VerifyPasswordAction()
/**
* Dismiss the current dialog.
*/
data object DismissDialog : VerifyPasswordAction()
/**
* Represents a click on the resend code button.
*/
data object ResendCodeClick : VerifyPasswordAction()
/**
* Represents a change in the password input.
* @param input The new password input.
@ -403,5 +547,17 @@ sealed class VerifyPasswordAction {
data class UnlockVaultResultReceive(
val vaultUnlockResult: VaultUnlockResult,
) : Internal()
/**
* Represents a result of requesting an OTP code.
* @param result The result of requesting an OTP code.
*/
data class SendOtpCodeResultReceive(val result: RequestOtpResult) : Internal()
/**
* Represents a result of verifying an OTP code.
* @param result The result of verifying an OTP code.
*/
data class VerifyOtpResultReceive(val result: VerifyOtpResult) : Internal()
}
}

View File

@ -10,8 +10,9 @@ import com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword.VerifyPas
*/
data class VerifyPasswordHandlers(
val onNavigateBackClick: () -> Unit,
val onUnlockClick: () -> Unit,
val onContinueClick: () -> Unit,
val onInputChanged: (String) -> Unit,
val onSendCodeClick: () -> Unit,
val onDismissDialog: () -> Unit,
) {
@ -26,14 +27,17 @@ data class VerifyPasswordHandlers(
onNavigateBackClick = {
viewModel.trySendAction(VerifyPasswordAction.NavigateBackClick)
},
onUnlockClick = {
viewModel.trySendAction(VerifyPasswordAction.UnlockClick)
onContinueClick = {
viewModel.trySendAction(VerifyPasswordAction.ContinueClick)
},
onInputChanged = {
viewModel.trySendAction(
VerifyPasswordAction.PasswordInputChangeReceive(it),
)
},
onSendCodeClick = {
viewModel.trySendAction(VerifyPasswordAction.ResendCodeClick)
},
onDismissDialog = {
viewModel.trySendAction(VerifyPasswordAction.DismissDialog)
},

View File

@ -1,5 +1,6 @@
package com.x8bit.bitwarden.ui.vault.feature.exportitems.verifypassword
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.filterToOne
@ -15,6 +16,7 @@ import androidx.compose.ui.test.performTextInput
import com.bitwarden.core.data.repository.util.bufferedMutableSharedFlow
import com.bitwarden.data.repository.model.Environment
import com.bitwarden.network.model.OrganizationType
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
import com.x8bit.bitwarden.data.auth.repository.model.Organization
@ -81,10 +83,34 @@ class VerifyPasswordScreenTest : BitwardenComposeTest() {
.onNodeWithText("You vault is locked. Verify your master password to continue.")
composeTestRule
.onNodeWithText("Unlock")
.onNodeWithText("Continue")
.assertIsNotEnabled()
}
@Test
fun `otp state should be correct`() = runTest {
mockStateFlow.emit(
DEFAULT_STATE.copy(
title = BitwardenString.verify_your_account_email_address.asText(),
subtext = BitwardenString
.enter_the_6_digit_code_that_was_emailed_to_the_address_below
.asText(),
showResendCodeButton = true,
),
)
composeTestRule
.onNodeWithText("Verify your account email address")
composeTestRule
.onNodeWithText("Enter the 6-digit code that was emailed to the address below")
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Resend code")
.assertIsDisplayed()
}
@Test
fun `input should update based on state`() = runTest {
composeTestRule
@ -105,10 +131,6 @@ class VerifyPasswordScreenTest : BitwardenComposeTest() {
composeTestRule
.onNodeWithText("Master password")
.performTextInput("abc123")
composeTestRule
.onNodeWithTag("PasswordVisibilityToggle")
.performClick()
verify {
viewModel.trySendAction(
VerifyPasswordAction.PasswordInputChangeReceive("abc123"),
@ -117,26 +139,37 @@ class VerifyPasswordScreenTest : BitwardenComposeTest() {
}
@Test
fun `Unlock button should should update based on input`() = runTest {
fun `Continue button should should update based on input`() = runTest {
composeTestRule
.onNodeWithText("Unlock")
.onNodeWithText("Continue")
.assertIsNotEnabled()
mockStateFlow.emit(DEFAULT_STATE.copy(input = "abc123"))
composeTestRule
.onNodeWithText("Unlock")
.onNodeWithText("Continue")
.assertIsEnabled()
}
@Test
fun `Unlock button should send UnlockClick action`() = runTest {
fun `Continue button should send ContinueClick action`() = runTest {
mockStateFlow.emit(DEFAULT_STATE.copy(input = "abc123"))
composeTestRule
.onNodeWithText("Unlock")
.onNodeWithText("Continue")
.performClick()
verify {
viewModel.trySendAction(VerifyPasswordAction.UnlockClick)
viewModel.trySendAction(VerifyPasswordAction.ContinueClick)
}
}
@Test
fun `Resend code button should send SendCodeClick action`() = runTest {
mockStateFlow.emit(DEFAULT_STATE.copy(showResendCodeButton = true))
composeTestRule
.onNodeWithText("Resend code")
.performClick()
verify {
viewModel.trySendAction(VerifyPasswordAction.ResendCodeClick)
}
}
@ -259,6 +292,8 @@ private val DEFAULT_ACCOUNT_SELECTION_LIST_ITEM = AccountSelectionListItem(
initials = DEFAULT_USER_STATE.activeAccount.initials,
)
private val DEFAULT_STATE = VerifyPasswordState(
title = BitwardenString.verify_your_master_password.asText(),
subtext = null,
accountSummaryListItem = DEFAULT_ACCOUNT_SELECTION_LIST_ITEM,
input = "",
dialog = null,

View File

@ -1135,4 +1135,6 @@ Do you want to switch to this account?</string>
<string name="your_vault_may_be_empty_or_import_some_item_types_isnt_supported">Your vault may be empty, or importing some item types isnt allowed for your account.</string>
<string name="no_items_available_to_import">No items available to import</string>
<string name="select_a_different_account">Select a different account</string>
<string name="verify_your_account_email_address">Verify your account email address</string>
<string name="enter_the_6_digit_code_that_was_emailed_to_the_address_below">Enter the 6-digit code that was emailed to the address below</string>
</resources>