mirror of
https://github.com/bitwarden/android.git
synced 2025-12-10 00:06:22 -06:00
[PM-26810] Add OTP support to VerifyPasswordScreen (#6034)
This commit is contained in:
parent
ae3470c598
commit
74aa0a78ec
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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 isn’t 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user