mirror of
https://github.com/bitwarden/android.git
synced 2024-11-21 17:05:44 +03:00
[PM-10685] Support keyboard Done event as CTA Unlock on Pin\Master Password unlock screen (#3691)
This commit is contained in:
parent
38e693f92c
commit
88b40cfd10
7 changed files with 112 additions and 0 deletions
|
@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -31,6 +32,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
@ -210,6 +212,12 @@ fun VaultUnlockScreen(
|
|||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
autoFocus = state.showKeyboard,
|
||||
imeAction = ImeAction.Done,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = remember(viewModel) {
|
||||
{ viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||
},
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
|||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.emptyInputDialogMessage
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenErrorMessage
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.BackgroundEvent
|
||||
|
@ -200,6 +201,18 @@ class VaultUnlockViewModel @Inject constructor(
|
|||
|
||||
private fun handleUnlockClick() {
|
||||
val activeUserId = authRepository.activeUserId ?: return
|
||||
|
||||
if (state.input.isEmpty()) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
it.vaultUnlockType.emptyInputDialogMessage,
|
||||
),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mutableStateFlow.update { it.copy(dialog = VaultUnlockState.VaultUnlockDialog.Loading) }
|
||||
viewModelScope.launch {
|
||||
val vaultUnlockResult = when (state.vaultUnlockType) {
|
||||
|
|
|
@ -72,3 +72,9 @@ val VaultUnlockType.unlockScreenKeyboardType: KeyboardType
|
|||
VaultUnlockType.MASTER_PASSWORD -> KeyboardType.Password
|
||||
VaultUnlockType.PIN -> KeyboardType.Number
|
||||
}
|
||||
|
||||
/**
|
||||
* The message to show when user try to unlock vault with empty or blank input.
|
||||
*/
|
||||
val VaultUnlockType.emptyInputDialogMessage: Text
|
||||
get() = R.string.validation_field_required.asText(unlockScreenInputLabel)
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.components.field
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
|
@ -52,6 +53,7 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
|||
* @param keyboardType The type of keyboard the user has access to when inputting values into
|
||||
* the password field.
|
||||
* @param imeAction the preferred IME action for the keyboard to have.
|
||||
* @param keyboardActions the callbacks of keyboard actions.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenPasswordField(
|
||||
|
@ -68,6 +70,7 @@ fun BitwardenPasswordField(
|
|||
autoFocus: Boolean = false,
|
||||
keyboardType: KeyboardType = KeyboardType.Password,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
OutlinedTextField(
|
||||
|
@ -89,6 +92,7 @@ fun BitwardenPasswordField(
|
|||
keyboardType = keyboardType,
|
||||
imeAction = imeAction,
|
||||
),
|
||||
keyboardActions = keyboardActions,
|
||||
supportingText = hint?.let {
|
||||
{
|
||||
Text(
|
||||
|
@ -144,6 +148,7 @@ fun BitwardenPasswordField(
|
|||
* @param keyboardType The type of keyboard the user has access to when inputting values into
|
||||
* the password field.
|
||||
* @param imeAction the preferred IME action for the keyboard to have.
|
||||
* @param keyboardActions the callbacks of keyboard actions.
|
||||
*/
|
||||
@Composable
|
||||
fun BitwardenPasswordField(
|
||||
|
@ -159,6 +164,7 @@ fun BitwardenPasswordField(
|
|||
autoFocus: Boolean = false,
|
||||
keyboardType: KeyboardType = KeyboardType.Password,
|
||||
imeAction: ImeAction = ImeAction.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions.Default,
|
||||
) {
|
||||
var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) }
|
||||
BitwardenPasswordField(
|
||||
|
@ -175,6 +181,7 @@ fun BitwardenPasswordField(
|
|||
autoFocus = autoFocus,
|
||||
keyboardType = keyboardType,
|
||||
imeAction = imeAction,
|
||||
keyboardActions = keyboardActions,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.vaultunlock
|
||||
|
||||
import androidx.compose.ui.input.key.KeyEvent
|
||||
import androidx.compose.ui.input.key.NativeKeyEvent
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsFocused
|
||||
|
@ -12,8 +14,10 @@ import androidx.compose.ui.test.onAllNodesWithText
|
|||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performKeyPress
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.requestFocus
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
|
@ -418,6 +422,19 @@ class VaultUnlockScreenTest : BaseComposeTest() {
|
|||
verify { viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `keyboard Done event should send UnlockClick action`() {
|
||||
val keyEvent = KeyEvent(
|
||||
NativeKeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_ENTER),
|
||||
)
|
||||
composeTestRule
|
||||
.onNodeWithText("Master password")
|
||||
.performScrollTo()
|
||||
.requestFocus()
|
||||
.performKeyPress(keyEvent)
|
||||
verify { viewModel.trySendAction(VaultUnlockAction.UnlockClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state with input and without biometrics should request focus on input field`() {
|
||||
mutableStateFlow.update { it.copy(hideInput = false, isBiometricEnabled = false) }
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFl
|
|||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.model.UnlockType
|
||||
import com.x8bit.bitwarden.ui.auth.feature.vaultunlock.util.unlockScreenInputLabel
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
|
@ -461,6 +462,28 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for empty password should display error dialog`() {
|
||||
val password = ""
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.MASTER_PASSWORD,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
R.string.validation_field_required.asText(
|
||||
initialState.vaultUnlockType.unlockScreenInputLabel,
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for password unlock should display error dialog on AuthenticationError`() {
|
||||
val password = "abcd1234"
|
||||
|
@ -598,6 +621,28 @@ class VaultUnlockViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for empty PIN should display error dialog`() {
|
||||
val password = ""
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
input = password,
|
||||
vaultUnlockType = VaultUnlockType.PIN,
|
||||
)
|
||||
val viewModel = createViewModel(state = initialState)
|
||||
|
||||
viewModel.trySendAction(VaultUnlockAction.UnlockClick)
|
||||
assertEquals(
|
||||
initialState.copy(
|
||||
dialog = VaultUnlockState.VaultUnlockDialog.Error(
|
||||
R.string.validation_field_required.asText(
|
||||
initialState.vaultUnlockType.unlockScreenInputLabel,
|
||||
),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on UnlockClick for PIN unlock should display error dialog on AuthenticationError`() {
|
||||
val pin = "1234"
|
||||
|
|
|
@ -77,4 +77,20 @@ class VaultUnlockTypeExtensionsTest {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emptyInputDialogMessage should return the correct title for each type`() {
|
||||
mapOf(
|
||||
VaultUnlockType.MASTER_PASSWORD to R.string.validation_field_required.asText(
|
||||
R.string.master_password.asText(),
|
||||
),
|
||||
VaultUnlockType.PIN to R.string.validation_field_required.asText(R.string.pin.asText()),
|
||||
)
|
||||
.forEach { (type, expected) ->
|
||||
assertEquals(
|
||||
expected,
|
||||
type.emptyInputDialogMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue