diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt index e4c9bccebc..03e7ffc0a0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.testTag import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -72,11 +71,26 @@ fun VaultUnlockScreen( val state by viewModel.stateFlow.collectAsStateWithLifecycle() val context = LocalContext.current val resources = context.resources + + val onBiometricsUnlockClick: () -> Unit = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) } + } + val onBiometricsLockOut: () -> Unit = remember(viewModel) { + { viewModel.trySendAction(VaultUnlockAction.BiometricsLockOut) } + } + EventsEffect(viewModel = viewModel) { event -> when (event) { is VaultUnlockEvent.ShowToast -> { Toast.makeText(context, event.text(resources), Toast.LENGTH_SHORT).show() } + + VaultUnlockEvent.PromptForBiometrics -> { + biometricsManager.promptForBiometrics( + onSuccess = onBiometricsUnlockClick, + onLockOut = onBiometricsLockOut, + ) + } } } @@ -121,12 +135,6 @@ fun VaultUnlockScreen( ) } - val onBiometricsUnlockClick: () -> Unit = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.BiometricsUnlockClick) } - } - val onBiometricsLockOut: () -> Unit = remember(viewModel) { - { viewModel.trySendAction(VaultUnlockAction.BiometricsLockOut) } - } // Content BitwardenScaffold( modifier = Modifier @@ -209,14 +217,8 @@ fun VaultUnlockScreen( BitwardenOutlinedButton( label = stringResource(id = R.string.use_biometrics_to_unlock), onClick = { - biometricsManager.promptBiometrics( + biometricsManager.promptForBiometrics( onSuccess = onBiometricsUnlockClick, - onCancel = { - // no-op - }, - onError = { - // no-op - }, onLockOut = onBiometricsLockOut, ) }, @@ -266,3 +268,22 @@ fun VaultUnlockScreen( } } } + +/** + * Helper method for easier prompting for biometrics. + */ +private fun BiometricsManager.promptForBiometrics( + onSuccess: () -> Unit, + onLockOut: () -> Unit, +) { + promptBiometrics( + onSuccess = onSuccess, + onCancel = { + // no-op + }, + onError = { + // no-op + }, + onLockOut = onLockOut, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt index 27bce56461..12678b1df8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModel.kt @@ -94,6 +94,10 @@ class VaultUnlockViewModel @Inject constructor( sendAction(VaultUnlockAction.Internal.UserStateUpdateReceive(userState = it)) } .launchIn(viewModelScope) + + if (state.showBiometricLogin) { + sendEvent(VaultUnlockEvent.PromptForBiometrics) + } } override fun handleAction(action: VaultUnlockAction) { @@ -336,6 +340,11 @@ sealed class VaultUnlockEvent { data class ShowToast( val text: Text, ) : VaultUnlockEvent() + + /** + * Prompts the user for biometrics unlock. + */ + data object PromptForBiometrics : VaultUnlockEvent() } /** diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt index 70a823a4e5..f65aa9dc7a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockScreenTest.kt @@ -75,6 +75,19 @@ class VaultUnlockScreenTest : BaseComposeTest() { } } + @Test + fun `on PromptForBiometrics should call launchUri on intentManager`() { + mutableEventFlow.tryEmit(VaultUnlockEvent.PromptForBiometrics) + verify { + biometricsManager.promptBiometrics( + onSuccess = any(), + onCancel = any(), + onError = any(), + onLockOut = any(), + ) + } + } + @Test fun `account icon click should show the account switcher`() { composeTestRule.assertSwitcherIsNotDisplayed( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt index 5dc3aff36f..133678494e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/vaultunlock/VaultUnlockViewModelTest.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.ui.auth.feature.vaultunlock import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test import com.x8bit.bitwarden.R import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson import com.x8bit.bitwarden.data.auth.repository.AuthRepository @@ -54,6 +55,19 @@ class VaultUnlockViewModelTest : BaseViewModelTest() { every { isBiometricIntegrityValid(userId = DEFAULT_USER_STATE.activeUserId) } returns true } + @Test + fun `on init with biometrics enabled and valid should emit PromptForBiometrics`() = runTest { + val initialState = DEFAULT_STATE.copy( + isBiometricEnabled = true, + isBiometricsValid = true, + ) + val viewModel = createViewModel(state = initialState) + + viewModel.eventFlow.test { + assertEquals(VaultUnlockEvent.PromptForBiometrics, awaitItem()) + } + } + @Test fun `initial state should be correct when not set`() { val viewModel = createViewModel()