feat: An option to not require biometric confirmation tap

This commit is contained in:
Artem Chepurnoy 2024-03-29 12:08:26 +02:00
parent 75d7eb96a0
commit 7670a040e3
No known key found for this signature in database
GPG Key ID: FAC37D0CF674043E
22 changed files with 338 additions and 59 deletions

View File

@ -50,6 +50,7 @@ import com.artemchep.keyguard.common.model.getOrNull
import com.artemchep.keyguard.common.usecase.AddCipherUsedPasskeyHistory import com.artemchep.keyguard.common.usecase.AddCipherUsedPasskeyHistory
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.GetCiphers import com.artemchep.keyguard.common.usecase.GetCiphers
import com.artemchep.keyguard.common.usecase.GetVaultSession import com.artemchep.keyguard.common.usecase.GetVaultSession
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
@ -536,6 +537,7 @@ fun produceUserVerificationState(
produceUserVerificationState( produceUserVerificationState(
onAuthenticated = onAuthenticated, onAuthenticated = onAuthenticated,
biometricStatusUseCase = instance(), biometricStatusUseCase = instance(),
getBiometricRequireConfirmation = instance(),
confirmAccessByPasswordUseCase = instance(), confirmAccessByPasswordUseCase = instance(),
windowCoroutineScope = instance(), windowCoroutineScope = instance(),
) )
@ -545,6 +547,7 @@ fun produceUserVerificationState(
fun produceUserVerificationState( fun produceUserVerificationState(
onAuthenticated: () -> Unit, onAuthenticated: () -> Unit,
biometricStatusUseCase: BiometricStatusUseCase, biometricStatusUseCase: BiometricStatusUseCase,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase, confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
windowCoroutineScope: WindowCoroutineScope, windowCoroutineScope: WindowCoroutineScope,
): UserVerificationState = produceScreenState( ): UserVerificationState = produceScreenState(
@ -567,8 +570,11 @@ fun produceUserVerificationState(
.getOrNull() .getOrNull()
when (biometricStatus) { when (biometricStatus) {
is BiometricStatus.Available -> { is BiometricStatus.Available -> {
val requireConfirmation = getBiometricRequireConfirmation()
.first()
createPromptOrNull( createPromptOrNull(
executor = executor, executor = executor,
requireConfirmation = requireConfirmation,
fn = { fn = {
onAuthenticated() onAuthenticated()
}, },
@ -652,11 +658,13 @@ fun produceUserVerificationState(
private fun createPromptOrNull( private fun createPromptOrNull(
executor: LoadingTask, executor: LoadingTask,
requireConfirmation: Boolean,
fn: () -> Unit, fn: () -> Unit,
): PureBiometricAuthPrompt = run { ): PureBiometricAuthPrompt = run {
BiometricAuthPromptSimple( BiometricAuthPromptSimple(
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title), title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text), text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
requireConfirmation = requireConfirmation,
onComplete = { result -> onComplete = { result ->
result.fold( result.fold(
ifLeft = { exception -> ifLeft = { exception ->

View File

@ -51,6 +51,7 @@ private fun FragmentActivity.launchPrompt(
?.also(::setDescription) ?.also(::setDescription)
} }
.setNegativeButtonText(getString(android.R.string.cancel)) .setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(event.requireConfirmation)
.build() .build()
val prompt = BiometricPrompt( val prompt = BiometricPrompt(
this, this,
@ -89,6 +90,7 @@ private fun FragmentActivity.launchPrompt(
?.also(::setDescription) ?.also(::setDescription)
} }
.setNegativeButtonText(getString(android.R.string.cancel)) .setNegativeButtonText(getString(android.R.string.cancel))
.setConfirmationRequired(event.requireConfirmation)
.build() .build()
val prompt = BiometricPrompt( val prompt = BiometricPrompt(
this, this,

View File

@ -10,6 +10,7 @@ class BiometricAuthPrompt(
val title: TextHolder, val title: TextHolder,
val text: TextHolder? = null, val text: TextHolder? = null,
val cipher: LeCipher, val cipher: LeCipher,
val requireConfirmation: Boolean,
/** /**
* Called when the user either failed the authentication or * Called when the user either failed the authentication or
* successfully passed it. * successfully passed it.
@ -22,6 +23,7 @@ class BiometricAuthPrompt(
class BiometricAuthPromptSimple( class BiometricAuthPromptSimple(
val title: TextHolder, val title: TextHolder,
val text: TextHolder? = null, val text: TextHolder? = null,
val requireConfirmation: Boolean,
/** /**
* Called when the user either failed the authentication or * Called when the user either failed the authentication or
* successfully passed it. * successfully passed it.

View File

@ -17,6 +17,7 @@ sealed interface VaultState {
class WithBiometric( class WithBiometric(
val getCipher: () -> Either<Throwable, LeCipher>, val getCipher: () -> Either<Throwable, LeCipher>,
val getCreateIo: (String) -> IO<Unit>, val getCreateIo: (String) -> IO<Unit>,
val requireConfirmation: Boolean,
) )
} }
@ -32,6 +33,7 @@ sealed interface VaultState {
class WithBiometric( class WithBiometric(
val getCipher: () -> Either<Throwable, LeCipher>, val getCipher: () -> Either<Throwable, LeCipher>,
val getCreateIo: () -> IO<Unit>, val getCreateIo: () -> IO<Unit>,
val requireConfirmation: Boolean,
) )
} }

View File

@ -35,6 +35,8 @@ interface SettingsReadRepository {
fun getBiometricTimeout(): Flow<Duration?> fun getBiometricTimeout(): Flow<Duration?>
fun getBiometricRequireConfirmation(): Flow<Boolean>
fun getClipboardClearDelay(): Flow<Duration?> fun getClipboardClearDelay(): Flow<Duration?>
fun getClipboardUpdateDuration(): Flow<Duration?> fun getClipboardUpdateDuration(): Flow<Duration?>

View File

@ -57,6 +57,10 @@ interface SettingsReadWriteRepository : SettingsReadRepository {
duration: Duration?, duration: Duration?,
): IO<Unit> ): IO<Unit>
fun setBiometricRequireConfirmation(
requireConfirmation: Boolean,
): IO<Unit>
fun setClipboardClearDelay( fun setClipboardClearDelay(
duration: Duration?, duration: Duration?,
): IO<Unit> ): IO<Unit>

View File

@ -47,6 +47,7 @@ class SettingsRepositoryImpl(
private const val KEY_VAULT_TIMEOUT = "vault_timeout" private const val KEY_VAULT_TIMEOUT = "vault_timeout"
private const val KEY_VAULT_SCREEN_LOCK = "vault_screen_lock" private const val KEY_VAULT_SCREEN_LOCK = "vault_screen_lock"
private const val KEY_BIOMETRIC_TIMEOUT = "biometric_timeout" private const val KEY_BIOMETRIC_TIMEOUT = "biometric_timeout"
private const val KEY_BIOMETRIC_REQUIRE_CONFIRMATION = "biometric_require_confirmation"
private const val KEY_CLIPBOARD_CLEAR_DELAY = "clipboard_clear_delay" private const val KEY_CLIPBOARD_CLEAR_DELAY = "clipboard_clear_delay"
private const val KEY_CLIPBOARD_UPDATE_DURATION = "clipboard_update_duration" private const val KEY_CLIPBOARD_UPDATE_DURATION = "clipboard_update_duration"
private const val KEY_CONCEAL_FIELDS = "conceal_fields" private const val KEY_CONCEAL_FIELDS = "conceal_fields"
@ -107,6 +108,8 @@ class SettingsRepositoryImpl(
private val biometricTimeoutPref = store.getLong(KEY_BIOMETRIC_TIMEOUT, NONE_DURATION) private val biometricTimeoutPref = store.getLong(KEY_BIOMETRIC_TIMEOUT, NONE_DURATION)
private val biometricRequireConfirmationPref = store.getBoolean(KEY_BIOMETRIC_REQUIRE_CONFIRMATION, true)
private val clipboardClearDelayPref = private val clipboardClearDelayPref =
store.getLong(KEY_CLIPBOARD_CLEAR_DELAY, NONE_DURATION) store.getLong(KEY_CLIPBOARD_CLEAR_DELAY, NONE_DURATION)
@ -277,6 +280,13 @@ class SettingsRepositoryImpl(
override fun getBiometricTimeout() = biometricTimeoutPref override fun getBiometricTimeout() = biometricTimeoutPref
.asDuration() .asDuration()
override fun setBiometricRequireConfirmation(
requireConfirmation: Boolean,
) = biometricRequireConfirmationPref
.setAndCommit(requireConfirmation)
override fun getBiometricRequireConfirmation() = biometricRequireConfirmationPref
override fun setClipboardClearDelay(duration: Duration?) = clipboardClearDelayPref override fun setClipboardClearDelay(duration: Duration?) = clipboardClearDelayPref
.setAndCommit(duration) .setAndCommit(duration)

View File

@ -0,0 +1,6 @@
package com.artemchep.keyguard.common.usecase
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration
interface GetBiometricRequireConfirmation : () -> Flow<Boolean>

View File

@ -0,0 +1,5 @@
package com.artemchep.keyguard.common.usecase
import com.artemchep.keyguard.common.io.IO
interface PutBiometricRequireConfirmation : (Boolean) -> IO<Unit>

View File

@ -0,0 +1,19 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.service.settings.SettingsReadRepository
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import kotlinx.coroutines.flow.Flow
import org.kodein.di.DirectDI
import org.kodein.di.instance
class GetBiometricRequireConfirmationImpl(
settingsReadRepository: SettingsReadRepository,
) : GetBiometricRequireConfirmation {
private val sharedFlow = settingsReadRepository.getBiometricRequireConfirmation()
constructor(directDI: DirectDI) : this(
settingsReadRepository = directDI.instance(),
)
override fun invoke(): Flow<Boolean> = sharedFlow
}

View File

@ -0,0 +1,18 @@
package com.artemchep.keyguard.common.usecase.impl
import com.artemchep.keyguard.common.io.IO
import com.artemchep.keyguard.common.service.settings.SettingsReadWriteRepository
import com.artemchep.keyguard.common.usecase.PutBiometricRequireConfirmation
import org.kodein.di.DirectDI
import org.kodein.di.instance
class PutBiometricRequireConfirmationImpl(
private val settingsReadWriteRepository: SettingsReadWriteRepository,
) : PutBiometricRequireConfirmation {
constructor(directDI: DirectDI) : this(
settingsReadWriteRepository = directDI.instance(),
)
override fun invoke(requireConfirmation: Boolean): IO<Unit> = settingsReadWriteRepository
.setBiometricRequireConfirmation(requireConfirmation)
}

View File

@ -35,6 +35,7 @@ import com.artemchep.keyguard.common.usecase.BiometricKeyEncryptUseCase
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.common.usecase.DisableBiometric import com.artemchep.keyguard.common.usecase.DisableBiometric
import com.artemchep.keyguard.common.usecase.GetBiometricRemainingDuration import com.artemchep.keyguard.common.usecase.GetBiometricRemainingDuration
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.GetVaultSession import com.artemchep.keyguard.common.usecase.GetVaultSession
import com.artemchep.keyguard.common.usecase.PutVaultSession import com.artemchep.keyguard.common.usecase.PutVaultSession
import com.artemchep.keyguard.common.usecase.UnlockUseCase import com.artemchep.keyguard.common.usecase.UnlockUseCase
@ -48,6 +49,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -69,6 +71,7 @@ class UnlockUseCaseImpl(
private val disableBiometric: DisableBiometric, private val disableBiometric: DisableBiometric,
private val keyReadWriteRepository: FingerprintReadWriteRepository, private val keyReadWriteRepository: FingerprintReadWriteRepository,
private val sessionMetadataReadWriteRepository: SessionMetadataReadWriteRepository, private val sessionMetadataReadWriteRepository: SessionMetadataReadWriteRepository,
private val getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
private val getBiometricRemainingDuration: GetBiometricRemainingDuration, private val getBiometricRemainingDuration: GetBiometricRemainingDuration,
private val biometricKeyEncryptUseCase: BiometricKeyEncryptUseCase, private val biometricKeyEncryptUseCase: BiometricKeyEncryptUseCase,
private val decryptBiometricKeyUseCase: BiometricKeyDecryptUseCase, private val decryptBiometricKeyUseCase: BiometricKeyDecryptUseCase,
@ -127,6 +130,7 @@ class UnlockUseCaseImpl(
disableBiometric = directDI.instance(), disableBiometric = directDI.instance(),
keyReadWriteRepository = directDI.instance(), keyReadWriteRepository = directDI.instance(),
sessionMetadataReadWriteRepository = directDI.instance(), sessionMetadataReadWriteRepository = directDI.instance(),
getBiometricRequireConfirmation = directDI.instance(),
getBiometricRemainingDuration = directDI.instance(), getBiometricRemainingDuration = directDI.instance(),
biometricKeyEncryptUseCase = directDI.instance(), biometricKeyEncryptUseCase = directDI.instance(),
decryptBiometricKeyUseCase = directDI.instance(), decryptBiometricKeyUseCase = directDI.instance(),
@ -163,7 +167,7 @@ class UnlockUseCaseImpl(
} }
} }
private fun createCreateVaultState( private suspend fun createCreateVaultState(
biometric: BiometricStatus, biometric: BiometricStatus,
): VaultState { ): VaultState {
return VaultState.Create( return VaultState.Create(
@ -194,6 +198,8 @@ class UnlockUseCaseImpl(
// the code in the block. This allows us to not // the code in the block. This allows us to not
// return the cipher from the action. // return the cipher from the action.
.memoize() .memoize()
val requireConfirmation = getBiometricRequireConfirmation()
.first()
VaultState.Create.WithBiometric( VaultState.Create.WithBiometric(
getCipher = getCipherForEncryption, getCipher = getCipherForEncryption,
getCreateIo = { password -> getCreateIo = { password ->
@ -231,6 +237,7 @@ class UnlockUseCaseImpl(
} }
.dispatchOn(Dispatchers.Default) .dispatchOn(Dispatchers.Default)
}, },
requireConfirmation = requireConfirmation,
) )
} else { } else {
null null
@ -238,7 +245,7 @@ class UnlockUseCaseImpl(
) )
} }
private fun createUnlockVaultState( private suspend fun createUnlockVaultState(
tokens: Fingerprint, tokens: Fingerprint,
biometric: BiometricStatus, biometric: BiometricStatus,
lockReason: String?, lockReason: String?,
@ -275,6 +282,8 @@ class UnlockUseCaseImpl(
// the code in the block. This allows us to not // the code in the block. This allows us to not
// return the cipher from the action. // return the cipher from the action.
.memoize() .memoize()
val requireConfirmation = getBiometricRequireConfirmation()
.first()
VaultState.Unlock.WithBiometric( VaultState.Unlock.WithBiometric(
getCipher = getCipherForDecryption, getCipher = getCipherForDecryption,
getCreateIo = { getCreateIo = {
@ -296,6 +305,7 @@ class UnlockUseCaseImpl(
.flatMap(::unlock) .flatMap(::unlock)
.dispatchOn(Dispatchers.Default) .dispatchOn(Dispatchers.Default)
}, },
requireConfirmation = requireConfirmation,
) )
} else { } else {
null null

View File

@ -9,6 +9,7 @@ import com.artemchep.keyguard.common.model.BiometricAuthPrompt
import com.artemchep.keyguard.common.model.Loadable import com.artemchep.keyguard.common.model.Loadable
import com.artemchep.keyguard.common.model.ToastMessage import com.artemchep.keyguard.common.model.ToastMessage
import com.artemchep.keyguard.common.model.VaultState import com.artemchep.keyguard.common.model.VaultState
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.UnlockUseCase import com.artemchep.keyguard.common.usecase.UnlockUseCase
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
import com.artemchep.keyguard.common.util.flow.EventFlow import com.artemchep.keyguard.common.util.flow.EventFlow
@ -21,6 +22,7 @@ import com.artemchep.keyguard.feature.navigation.state.produceScreenState
import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.Res
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -37,6 +39,7 @@ private const val KEY_BIOMETRIC_ENABLED = "biometric.enabled"
fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct) { fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct) {
changePasswordState( changePasswordState(
unlockUseCase = instance(), unlockUseCase = instance(),
getBiometricRequireConfirmation = instance(),
windowCoroutineScope = instance(), windowCoroutineScope = instance(),
) )
} }
@ -44,6 +47,7 @@ fun changePasswordState(): Loadable<ChangePasswordState> = with(localDI().direct
@Composable @Composable
fun changePasswordState( fun changePasswordState(
unlockUseCase: UnlockUseCase, unlockUseCase: UnlockUseCase,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
windowCoroutineScope: WindowCoroutineScope, windowCoroutineScope: WindowCoroutineScope,
): Loadable<ChangePasswordState> = produceScreenState( ): Loadable<ChangePasswordState> = produceScreenState(
key = "change_password", key = "change_password",
@ -63,11 +67,16 @@ fun changePasswordState(
val fn = unlockUseCase() val fn = unlockUseCase()
.map { vaultState -> .map { vaultState ->
when (vaultState) { when (vaultState) {
is VaultState.Main -> ah( is VaultState.Main -> {
state = vaultState, val requireConfirmation = getBiometricRequireConfirmation()
biometricPromptSink = biometricPromptSink, .first()
windowCoroutineScope = windowCoroutineScope, ah(
) state = vaultState,
requireConfirmation = requireConfirmation,
biometricPromptSink = biometricPromptSink,
windowCoroutineScope = windowCoroutineScope,
)
}
else -> null else -> null
} }
@ -154,6 +163,7 @@ fun changePasswordState(
private fun RememberStateFlowScope.ah( private fun RememberStateFlowScope.ah(
state: VaultState.Main, state: VaultState.Main,
requireConfirmation: Boolean,
biometricPromptSink: EventFlow<BiometricAuthPrompt>, biometricPromptSink: EventFlow<BiometricAuthPrompt>,
windowCoroutineScope: WindowCoroutineScope, windowCoroutineScope: WindowCoroutineScope,
) = Fn( ) = Fn(
@ -184,6 +194,7 @@ private fun RememberStateFlowScope.ah(
val prompt = BiometricAuthPrompt( val prompt = BiometricAuthPrompt(
title = TextHolder.Res(Res.strings.changepassword_biometric_auth_confirm_title), title = TextHolder.Res(Res.strings.changepassword_biometric_auth_confirm_title),
cipher = cipher, cipher = cipher,
requireConfirmation = requireConfirmation,
onComplete = { result -> onComplete = { result ->
result.fold( result.fold(
ifLeft = { e -> ifLeft = { e ->

View File

@ -14,6 +14,7 @@ import com.artemchep.keyguard.common.model.PureBiometricAuthPrompt
import com.artemchep.keyguard.common.model.ToastMessage import com.artemchep.keyguard.common.model.ToastMessage
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase import com.artemchep.keyguard.common.usecase.ConfirmAccessByPasswordUseCase
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
import com.artemchep.keyguard.common.util.flow.EventFlow import com.artemchep.keyguard.common.util.flow.EventFlow
import com.artemchep.keyguard.feature.auth.common.TextFieldModel2 import com.artemchep.keyguard.feature.auth.common.TextFieldModel2
@ -28,6 +29,7 @@ import com.artemchep.keyguard.feature.navigation.state.produceScreenState
import com.artemchep.keyguard.res.Res import com.artemchep.keyguard.res.Res
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import org.kodein.di.compose.localDI import org.kodein.di.compose.localDI
@ -43,6 +45,7 @@ fun produceElevatedAccessState(
produceElevatedAccessState( produceElevatedAccessState(
transmitter = transmitter, transmitter = transmitter,
biometricStatusUseCase = instance(), biometricStatusUseCase = instance(),
getBiometricRequireConfirmation = instance(),
confirmAccessByPasswordUseCase = instance(), confirmAccessByPasswordUseCase = instance(),
windowCoroutineScope = instance(), windowCoroutineScope = instance(),
) )
@ -52,6 +55,7 @@ fun produceElevatedAccessState(
fun produceElevatedAccessState( fun produceElevatedAccessState(
transmitter: RouteResultTransmitter<ElevatedAccessResult>, transmitter: RouteResultTransmitter<ElevatedAccessResult>,
biometricStatusUseCase: BiometricStatusUseCase, biometricStatusUseCase: BiometricStatusUseCase,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase, confirmAccessByPasswordUseCase: ConfirmAccessByPasswordUseCase,
windowCoroutineScope: WindowCoroutineScope, windowCoroutineScope: WindowCoroutineScope,
): ElevatedAccessState = produceScreenState( ): ElevatedAccessState = produceScreenState(
@ -74,8 +78,11 @@ fun produceElevatedAccessState(
.getOrNull() .getOrNull()
when (biometricStatus) { when (biometricStatus) {
is BiometricStatus.Available -> { is BiometricStatus.Available -> {
val requireConfirmation = getBiometricRequireConfirmation()
.first()
createPromptOrNull( createPromptOrNull(
executor = executor, executor = executor,
requireConfirmation = requireConfirmation,
fn = { fn = {
navigatePopSelf() navigatePopSelf()
transmitter(ElevatedAccessResult.Allow) transmitter(ElevatedAccessResult.Allow)
@ -165,11 +172,13 @@ fun produceElevatedAccessState(
private fun createPromptOrNull( private fun createPromptOrNull(
executor: LoadingTask, executor: LoadingTask,
requireConfirmation: Boolean,
fn: () -> Unit, fn: () -> Unit,
): PureBiometricAuthPrompt = run { ): PureBiometricAuthPrompt = run {
BiometricAuthPromptSimple( BiometricAuthPromptSimple(
title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title), title = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_title),
text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text), text = TextHolder.Res(Res.strings.elevatedaccess_biometric_auth_confirm_text),
requireConfirmation = requireConfirmation,
onComplete = { result -> onComplete = { result ->
result.fold( result.fold(
ifLeft = { exception -> ifLeft = { exception ->

View File

@ -34,6 +34,7 @@ import com.artemchep.keyguard.feature.home.settings.component.settingAutofillRes
import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveRequestProvider import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveRequestProvider
import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveUriProvider import com.artemchep.keyguard.feature.home.settings.component.settingAutofillSaveUriProvider
import com.artemchep.keyguard.feature.home.settings.component.settingBiometricsProvider import com.artemchep.keyguard.feature.home.settings.component.settingBiometricsProvider
import com.artemchep.keyguard.feature.home.settings.component.settingBiometricsRequireConfirmationProvider
import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedPasswordsProvider import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedPasswordsProvider
import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedServicesProvider import com.artemchep.keyguard.feature.home.settings.component.settingCheckPwnedServicesProvider
import com.artemchep.keyguard.feature.home.settings.component.settingCheckTwoFAProvider import com.artemchep.keyguard.feature.home.settings.component.settingCheckTwoFAProvider
@ -130,6 +131,7 @@ object Setting {
const val PERMISSION_WRITE_EXTERNAL_STORAGE = "permission_write_external_storage" const val PERMISSION_WRITE_EXTERNAL_STORAGE = "permission_write_external_storage"
const val PERMISSION_POST_NOTIFICATION = "permission_post_notification" const val PERMISSION_POST_NOTIFICATION = "permission_post_notification"
const val BIOMETRIC = "biometric" const val BIOMETRIC = "biometric"
const val BIOMETRIC_REQUIRE_CONFIRMATION = "biometric_require_confirmation"
const val VAULT_PERSIST = "vault_persist" const val VAULT_PERSIST = "vault_persist"
const val VAULT_LOCK = "vault_lock" const val VAULT_LOCK = "vault_lock"
const val VAULT_LOCK_AFTER_REBOOT = "vault_lock_after_reboot" const val VAULT_LOCK_AFTER_REBOOT = "vault_lock_after_reboot"
@ -211,6 +213,7 @@ val hub = mapOf<String, (DirectDI) -> SettingComponent>(
Setting.PERMISSION_POST_NOTIFICATION to ::settingPermissionPostNotificationsProvider, Setting.PERMISSION_POST_NOTIFICATION to ::settingPermissionPostNotificationsProvider,
Setting.PERMISSION_WRITE_EXTERNAL_STORAGE to ::settingPermissionWriteExternalStorageProvider, Setting.PERMISSION_WRITE_EXTERNAL_STORAGE to ::settingPermissionWriteExternalStorageProvider,
Setting.BIOMETRIC to ::settingBiometricsProvider, Setting.BIOMETRIC to ::settingBiometricsProvider,
Setting.BIOMETRIC_REQUIRE_CONFIRMATION to ::settingBiometricsRequireConfirmationProvider,
Setting.VAULT_PERSIST to ::settingVaultPersistProvider, Setting.VAULT_PERSIST to ::settingVaultPersistProvider,
Setting.VAULT_LOCK to ::settingVaultLockProvider, Setting.VAULT_LOCK to ::settingVaultLockProvider,
Setting.VAULT_LOCK_AFTER_REBOOT to ::settingVaultLockAfterRebootProvider, Setting.VAULT_LOCK_AFTER_REBOOT to ::settingVaultLockAfterRebootProvider,

View File

@ -18,6 +18,7 @@ import com.artemchep.keyguard.common.service.vault.FingerprintReadRepository
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.common.usecase.DisableBiometric import com.artemchep.keyguard.common.usecase.DisableBiometric
import com.artemchep.keyguard.common.usecase.EnableBiometric import com.artemchep.keyguard.common.usecase.EnableBiometric
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
import com.artemchep.keyguard.common.util.flow.EventFlow import com.artemchep.keyguard.common.util.flow.EventFlow
import com.artemchep.keyguard.feature.biometric.BiometricPromptEffect import com.artemchep.keyguard.feature.biometric.BiometricPromptEffect
@ -26,6 +27,7 @@ import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.ui.FlatItem import com.artemchep.keyguard.ui.FlatItem
import com.artemchep.keyguard.ui.icons.icon import com.artemchep.keyguard.ui.icons.icon
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ -38,6 +40,7 @@ fun settingBiometricsProvider(
) = settingBiometricsProvider( ) = settingBiometricsProvider(
fingerprintReadRepository = directDI.instance(), fingerprintReadRepository = directDI.instance(),
biometricStatusUseCase = directDI.instance(), biometricStatusUseCase = directDI.instance(),
getBiometricRequireConfirmation = directDI.instance(),
enableBiometric = directDI.instance(), enableBiometric = directDI.instance(),
disableBiometric = directDI.instance(), disableBiometric = directDI.instance(),
windowCoroutineScope = directDI.instance(), windowCoroutineScope = directDI.instance(),
@ -46,6 +49,7 @@ fun settingBiometricsProvider(
fun settingBiometricsProvider( fun settingBiometricsProvider(
fingerprintReadRepository: FingerprintReadRepository, fingerprintReadRepository: FingerprintReadRepository,
biometricStatusUseCase: BiometricStatusUseCase, biometricStatusUseCase: BiometricStatusUseCase,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
enableBiometric: EnableBiometric, enableBiometric: EnableBiometric,
disableBiometric: DisableBiometric, disableBiometric: DisableBiometric,
windowCoroutineScope: WindowCoroutineScope, windowCoroutineScope: WindowCoroutineScope,
@ -54,64 +58,82 @@ fun settingBiometricsProvider(
.distinctUntilChanged() .distinctUntilChanged()
.flatMapLatest { hasBiometrics -> .flatMapLatest { hasBiometrics ->
if (hasBiometrics) { if (hasBiometrics) {
fingerprintReadRepository.get() createSettingComponentFlow(
.map { tokens -> fingerprintReadRepository = fingerprintReadRepository,
val hasBiometricEnabled = tokens?.biometric != null getBiometricRequireConfirmation = getBiometricRequireConfirmation,
enableBiometric = enableBiometric,
SettingIi( disableBiometric = disableBiometric,
search = SettingIi.Search( windowCoroutineScope = windowCoroutineScope,
group = "biometric", )
tokens = listOf(
"biometric",
),
),
) {
val promptSink = remember {
EventFlow<BiometricAuthPrompt>()
}
SettingBiometrics(
checked = hasBiometricEnabled,
onCheckedChange = { shouldBeChecked ->
if (shouldBeChecked) {
enableBiometric(null) // use global session
.map { d ->
val cipher = d.getCipher()
val prompt = BiometricAuthPrompt(
title = TextHolder.Res(Res.strings.pref_item_biometric_unlock_confirm_title),
cipher = cipher,
onComplete = { result ->
result.fold(
ifLeft = { exception ->
val message = exception.message
// biometricPromptErrorSink.emit(message)
},
ifRight = {
d.getCreateIo()
.launchIn(windowCoroutineScope)
},
)
},
)
promptSink.emit(prompt)
}
.launchIn(windowCoroutineScope)
} else {
disableBiometric()
.launchIn(windowCoroutineScope)
}
},
)
BiometricPromptEffect(promptSink)
}
}
} else { } else {
// hide option if the device does not have biometrics // hide option if the device does not have biometrics
flowOf(null) flowOf(null)
} }
} }
private fun createSettingComponentFlow(
fingerprintReadRepository: FingerprintReadRepository,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
enableBiometric: EnableBiometric,
disableBiometric: DisableBiometric,
windowCoroutineScope: WindowCoroutineScope,
) = combine(
getBiometricRequireConfirmation(),
fingerprintReadRepository.get()
.map { tokens ->
tokens?.biometric != null
},
) { requireConfirmation, biometrics ->
SettingIi(
search = SettingIi.Search(
group = "biometric",
tokens = listOf(
"biometric",
),
),
) {
val promptSink = remember {
EventFlow<BiometricAuthPrompt>()
}
SettingBiometrics(
checked = biometrics,
onCheckedChange = { shouldBeChecked ->
if (shouldBeChecked) {
enableBiometric(null) // use global session
.map { d ->
val cipher = d.getCipher()
val prompt = BiometricAuthPrompt(
title = TextHolder.Res(Res.strings.pref_item_biometric_unlock_confirm_title),
cipher = cipher,
requireConfirmation = requireConfirmation,
onComplete = { result ->
result.fold(
ifLeft = { exception ->
val message = exception.message
// biometricPromptErrorSink.emit(message)
},
ifRight = {
d.getCreateIo()
.launchIn(windowCoroutineScope)
},
)
},
)
promptSink.emit(prompt)
}
.launchIn(windowCoroutineScope)
} else {
disableBiometric()
.launchIn(windowCoroutineScope)
}
},
)
BiometricPromptEffect(promptSink)
}
}
@Composable @Composable
private fun SettingBiometrics( private fun SettingBiometrics(
checked: Boolean, checked: Boolean,

View File

@ -0,0 +1,123 @@
package com.artemchep.keyguard.feature.home.settings.component
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import arrow.core.partially1
import com.artemchep.keyguard.common.io.launchIn
import com.artemchep.keyguard.common.model.BiometricStatus
import com.artemchep.keyguard.common.service.vault.FingerprintReadRepository
import com.artemchep.keyguard.common.usecase.BiometricStatusUseCase
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.PutBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.WindowCoroutineScope
import com.artemchep.keyguard.res.Res
import com.artemchep.keyguard.ui.FlatItem
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import org.kodein.di.DirectDI
import org.kodein.di.instance
fun settingBiometricsRequireConfirmationProvider(
directDI: DirectDI,
) = settingBiometricsRequireConfirmationProvider(
fingerprintReadRepository = directDI.instance(),
biometricStatusUseCase = directDI.instance(),
getBiometricRequireConfirmation = directDI.instance(),
putBiometricRequireConfirmation = directDI.instance(),
windowCoroutineScope = directDI.instance(),
)
fun settingBiometricsRequireConfirmationProvider(
fingerprintReadRepository: FingerprintReadRepository,
biometricStatusUseCase: BiometricStatusUseCase,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
putBiometricRequireConfirmation: PutBiometricRequireConfirmation,
windowCoroutineScope: WindowCoroutineScope,
): SettingComponent = biometricStatusUseCase()
.map { it is BiometricStatus.Available }
.distinctUntilChanged()
.flatMapLatest { hasBiometrics ->
if (hasBiometrics) {
createSettingComponentFlow(
fingerprintReadRepository = fingerprintReadRepository,
getBiometricRequireConfirmation = getBiometricRequireConfirmation,
putBiometricRequireConfirmation = putBiometricRequireConfirmation,
windowCoroutineScope = windowCoroutineScope,
)
} else {
// hide option if the device does not have biometrics
flowOf(null)
}
}
private fun createSettingComponentFlow(
fingerprintReadRepository: FingerprintReadRepository,
getBiometricRequireConfirmation: GetBiometricRequireConfirmation,
putBiometricRequireConfirmation: PutBiometricRequireConfirmation,
windowCoroutineScope: WindowCoroutineScope,
) = combine(
getBiometricRequireConfirmation(),
fingerprintReadRepository.get()
.map { tokens ->
tokens?.biometric != null
},
) { requireConfirmation, biometrics ->
val onCheckedChange = { shouldRequireConfirmation: Boolean ->
putBiometricRequireConfirmation(shouldRequireConfirmation)
.launchIn(windowCoroutineScope)
Unit
}
SettingIi(
search = SettingIi.Search(
group = "biometric",
tokens = listOf(
"biometric",
"confirmation",
),
),
) {
SettingBiometricsRequireConfirmation(
checked = requireConfirmation,
onCheckedChange = onCheckedChange.takeIf { biometrics },
)
}
}
@Composable
private fun SettingBiometricsRequireConfirmation(
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit)?,
) {
FlatItem(
trailing = {
CompositionLocalProvider(
LocalMinimumInteractiveComponentEnforcement provides false,
) {
Switch(
checked = checked,
enabled = onCheckedChange != null,
onCheckedChange = onCheckedChange,
)
}
},
title = {
Text(
text = stringResource(Res.strings.pref_item_biometric_unlock_require_confirmation_title),
)
},
text = {
Text(
text = stringResource(Res.strings.pref_item_biometric_unlock_require_confirmation_text),
)
},
onClick = onCheckedChange?.partially1(!checked),
)
}

View File

@ -36,6 +36,7 @@ fun SecuritySettingsScreen() {
key = "biometric", key = "biometric",
list = listOf( list = listOf(
SettingPaneItem.Item(Setting.BIOMETRIC), SettingPaneItem.Item(Setting.BIOMETRIC),
SettingPaneItem.Item(Setting.BIOMETRIC_REQUIRE_CONFIRMATION),
SettingPaneItem.Item(Setting.REQUIRE_MASTER_PASSWORD), SettingPaneItem.Item(Setting.REQUIRE_MASTER_PASSWORD),
), ),
), ),

View File

@ -144,6 +144,7 @@ private fun createPromptOrNull(
BiometricAuthPrompt( BiometricAuthPrompt(
title = TextHolder.Res(Res.strings.setup_biometric_auth_confirm_title), title = TextHolder.Res(Res.strings.setup_biometric_auth_confirm_title),
cipher = cipher, cipher = cipher,
requireConfirmation = createVaultWithMasterPasswordAndBiometricFn.requireConfirmation,
onComplete = { result -> onComplete = { result ->
result.fold( result.fold(
ifLeft = { exception -> ifLeft = { exception ->
@ -185,6 +186,7 @@ private class CreateVaultWithBiometric(
*/ */
val getCipher: () -> Either<Throwable, LeCipher>, val getCipher: () -> Either<Throwable, LeCipher>,
getCreateIo: (String) -> IO<Unit>, getCreateIo: (String) -> IO<Unit>,
val requireConfirmation: Boolean,
) : CreateVaultWithPassword(executor, getCreateIo) { ) : CreateVaultWithPassword(executor, getCreateIo) {
// Create from vault state options // Create from vault state options
constructor( constructor(
@ -194,6 +196,7 @@ private class CreateVaultWithBiometric(
executor = executor, executor = executor,
getCipher = options.getCipher, getCipher = options.getCipher,
getCreateIo = options.getCreateIo, getCreateIo = options.getCreateIo,
requireConfirmation = options.requireConfirmation,
) )
} }

View File

@ -201,6 +201,7 @@ private fun createPromptOrNull(
title = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_title), title = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_title),
text = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_text), text = TextHolder.Res(Res.strings.unlock_biometric_auth_confirm_text),
cipher = cipher, cipher = cipher,
requireConfirmation = fn.requireConfirmation,
onComplete = { result -> onComplete = { result ->
result.fold( result.fold(
ifLeft = { exception -> ifLeft = { exception ->
@ -249,6 +250,7 @@ private class UnlockVaultWithBiometric(
*/ */
val getCipher: () -> Either<Throwable, LeCipher>, val getCipher: () -> Either<Throwable, LeCipher>,
private val getCreateIo: () -> IO<Unit>, private val getCreateIo: () -> IO<Unit>,
val requireConfirmation: Boolean,
) : () -> Unit { ) : () -> Unit {
// Create from vault state options // Create from vault state options
constructor( constructor(
@ -258,6 +260,7 @@ private class UnlockVaultWithBiometric(
executor = executor, executor = executor,
getCipher = options.getCipher, getCipher = options.getCipher,
getCreateIo = options.getCreateIo, getCreateIo = options.getCreateIo,
requireConfirmation = options.requireConfirmation,
) )
override fun invoke() { override fun invoke() {

View File

@ -991,6 +991,8 @@
<string name="pref_item_features_overview_title">Features overview</string> <string name="pref_item_features_overview_title">Features overview</string>
<string name="pref_item_url_override_title">URL overrides</string> <string name="pref_item_url_override_title">URL overrides</string>
<string name="pref_item_biometric_unlock_title">Biometric unlock</string> <string name="pref_item_biometric_unlock_title">Biometric unlock</string>
<string name="pref_item_biometric_unlock_require_confirmation_title">Require confirmation</string>
<string name="pref_item_biometric_unlock_require_confirmation_text">Require an additional confirmation button tap after a successful biometric authentication</string>
<!-- <!--
A title of the system popup that asks a user to use his biometric to later A title of the system popup that asks a user to use his biometric to later
be able to use it to unlock the vault. --> be able to use it to unlock the vault. -->

View File

@ -100,6 +100,7 @@ import com.artemchep.keyguard.common.usecase.GetAutofillManualSelection
import com.artemchep.keyguard.common.usecase.GetAutofillRespectAutofillOff import com.artemchep.keyguard.common.usecase.GetAutofillRespectAutofillOff
import com.artemchep.keyguard.common.usecase.GetAutofillSaveRequest import com.artemchep.keyguard.common.usecase.GetAutofillSaveRequest
import com.artemchep.keyguard.common.usecase.GetAutofillSaveUri import com.artemchep.keyguard.common.usecase.GetAutofillSaveUri
import com.artemchep.keyguard.common.usecase.GetBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.GetBiometricTimeout import com.artemchep.keyguard.common.usecase.GetBiometricTimeout
import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants import com.artemchep.keyguard.common.usecase.GetBiometricTimeoutVariants
import com.artemchep.keyguard.common.usecase.GetCachePremium import com.artemchep.keyguard.common.usecase.GetCachePremium
@ -162,6 +163,7 @@ import com.artemchep.keyguard.common.usecase.PutAutofillManualSelection
import com.artemchep.keyguard.common.usecase.PutAutofillRespectAutofillOff import com.artemchep.keyguard.common.usecase.PutAutofillRespectAutofillOff
import com.artemchep.keyguard.common.usecase.PutAutofillSaveRequest import com.artemchep.keyguard.common.usecase.PutAutofillSaveRequest
import com.artemchep.keyguard.common.usecase.PutAutofillSaveUri import com.artemchep.keyguard.common.usecase.PutAutofillSaveUri
import com.artemchep.keyguard.common.usecase.PutBiometricRequireConfirmation
import com.artemchep.keyguard.common.usecase.PutBiometricTimeout import com.artemchep.keyguard.common.usecase.PutBiometricTimeout
import com.artemchep.keyguard.common.usecase.PutCachePremium import com.artemchep.keyguard.common.usecase.PutCachePremium
import com.artemchep.keyguard.common.usecase.PutCheckPwnedPasswords import com.artemchep.keyguard.common.usecase.PutCheckPwnedPasswords
@ -222,6 +224,7 @@ import com.artemchep.keyguard.common.usecase.impl.GetAutofillManualSelectionImpl
import com.artemchep.keyguard.common.usecase.impl.GetAutofillRespectAutofillOffImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillRespectAutofillOffImpl
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveRequestImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveRequestImpl
import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveUriImpl import com.artemchep.keyguard.common.usecase.impl.GetAutofillSaveUriImpl
import com.artemchep.keyguard.common.usecase.impl.GetBiometricRequireConfirmationImpl
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutImpl import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutImpl
import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl import com.artemchep.keyguard.common.usecase.impl.GetBiometricTimeoutVariantsImpl
import com.artemchep.keyguard.common.usecase.impl.GetCachePremiumImpl import com.artemchep.keyguard.common.usecase.impl.GetCachePremiumImpl
@ -281,6 +284,7 @@ import com.artemchep.keyguard.common.usecase.impl.PutAutofillManualSelectionImpl
import com.artemchep.keyguard.common.usecase.impl.PutAutofillRespectAutofillOffImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillRespectAutofillOffImpl
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveRequestImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveRequestImpl
import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveUriImpl import com.artemchep.keyguard.common.usecase.impl.PutAutofillSaveUriImpl
import com.artemchep.keyguard.common.usecase.impl.PutBiometricRequireConfirmationImpl
import com.artemchep.keyguard.common.usecase.impl.PutBiometricTimeoutImpl import com.artemchep.keyguard.common.usecase.impl.PutBiometricTimeoutImpl
import com.artemchep.keyguard.common.usecase.impl.PutCachePremiumImpl import com.artemchep.keyguard.common.usecase.impl.PutCachePremiumImpl
import com.artemchep.keyguard.common.usecase.impl.PutCheckPwnedPasswordsImpl import com.artemchep.keyguard.common.usecase.impl.PutCheckPwnedPasswordsImpl
@ -477,6 +481,16 @@ fun globalModuleJvm() = DI.Module(
directDI = this, directDI = this,
) )
} }
bindSingleton<GetBiometricRequireConfirmation> {
GetBiometricRequireConfirmationImpl(
directDI = this,
)
}
bindSingleton<PutBiometricRequireConfirmation> {
PutBiometricRequireConfirmationImpl(
directDI = this,
)
}
bindSingleton<GetCheckPwnedPasswords> { bindSingleton<GetCheckPwnedPasswords> {
GetCheckPwnedPasswordsImpl( GetCheckPwnedPasswordsImpl(
directDI = this, directDI = this,