diff --git a/core/presentation/build.gradle.kts b/core/presentation/build.gradle.kts index 4fcf91320..bd4787c2c 100644 --- a/core/presentation/build.gradle.kts +++ b/core/presentation/build.gradle.kts @@ -47,6 +47,7 @@ kotlin { } androidMain.dependencies { implementation(libs.androidx.appcompat) + implementation(libs.androidx.biometric) } val androidHostTest by getting { dependencies { diff --git a/core/presentation/src/androidHostTest/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModelTest.kt b/core/presentation/src/androidHostTest/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModelTest.kt index f8bae95b2..346d1f7a4 100644 --- a/core/presentation/src/androidHostTest/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModelTest.kt +++ b/core/presentation/src/androidHostTest/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModelTest.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.autofill.AutofillManager import app.cash.turbine.test import com.softartdev.notedelight.StubEditable import com.softartdev.notedelight.anyObject +import com.softartdev.notedelight.interactor.BiometricAuthService import com.softartdev.notedelight.navigation.AppNavGraph import com.softartdev.notedelight.navigation.Router import com.softartdev.notedelight.presentation.MainDispatcherRule @@ -29,12 +30,14 @@ class SignInViewModelTest { private val mockCheckPasswordUseCase = Mockito.mock(CheckPasswordUseCase::class.java) private val mockRouter = Mockito.mock(Router::class.java) private val mockAutofillManager = Mockito.mock(AutofillManager::class.java) + private val mockBiometricAuthService = Mockito.mock(BiometricAuthService::class.java) private lateinit var signInViewModel: SignInViewModel @Before fun setUp() { - signInViewModel = SignInViewModel(mockCheckPasswordUseCase, mockRouter) + Mockito.`when`(mockBiometricAuthService.isBiometricAvailable()).thenReturn(false) + signInViewModel = SignInViewModel(mockCheckPasswordUseCase, mockRouter, mockBiometricAuthService) signInViewModel.autofillManager = mockAutofillManager } @@ -114,4 +117,4 @@ class SignInViewModelTest { cancelAndIgnoreRemainingEvents() } } -} \ No newline at end of file +} diff --git a/core/presentation/src/androidMain/kotlin/com/softartdev/notedelight/interactor/AndroidBiometricAuthService.kt b/core/presentation/src/androidMain/kotlin/com/softartdev/notedelight/interactor/AndroidBiometricAuthService.kt new file mode 100644 index 000000000..5b2bbe2f9 --- /dev/null +++ b/core/presentation/src/androidMain/kotlin/com/softartdev/notedelight/interactor/AndroidBiometricAuthService.kt @@ -0,0 +1,66 @@ +package com.softartdev.notedelight.interactor + +import android.content.Context +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import androidx.fragment.app.FragmentActivity +import androidx.core.content.ContextCompat +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +class AndroidBiometricAuthService(private val context: Context) : BiometricAuthService { + + override suspend fun isBiometricAvailable(): Boolean { + val biometricManager = BiometricManager.from(context) + val authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG + return biometricManager.canAuthenticate(authenticators) == BiometricManager.BIOMETRIC_SUCCESS + } + + override suspend fun authenticate(): BiometricAuthResult = suspendCancellableCoroutine { continuation -> + val activity = context.findActivity() as? FragmentActivity + if (activity == null) { + continuation.resume(BiometricAuthResult.FallbackToPassword) + return@suspendCancellableCoroutine + } + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle("Biometric authentication") + .setSubtitle("Sign in to NoteDelight") + .setNegativeButtonText("Use password") + .build() + val biometricPrompt = BiometricPrompt( + activity, + ContextCompat.getMainExecutor(activity), + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + if (continuation.isActive) { + continuation.resume(BiometricAuthResult.Success) + } + } + + override fun onAuthenticationFailed() { + if (continuation.isActive) { + continuation.resume(BiometricAuthResult.Failed) + } + } + + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + if (!continuation.isActive) return + val authResult = when (errorCode) { + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_CANCELED -> BiometricAuthResult.FallbackToPassword + else -> BiometricAuthResult.Failed + } + continuation.resume(authResult) + } + } + ) + biometricPrompt.authenticate(promptInfo) + } +} + +private tailrec fun Context.findActivity(): android.app.Activity? = when (this) { + is android.app.Activity -> this + is android.content.ContextWrapper -> baseContext.findActivity() + else -> null +} diff --git a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/interactor/BiometricAuthService.kt b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/interactor/BiometricAuthService.kt new file mode 100644 index 000000000..0c957558d --- /dev/null +++ b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/interactor/BiometricAuthService.kt @@ -0,0 +1,12 @@ +package com.softartdev.notedelight.interactor + +interface BiometricAuthService { + suspend fun isBiometricAvailable(): Boolean + suspend fun authenticate(): BiometricAuthResult +} + +sealed interface BiometricAuthResult { + data object Success : BiometricAuthResult + data object Failed : BiometricAuthResult + data object FallbackToPassword : BiometricAuthResult +} diff --git a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInAction.kt b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInAction.kt index c26afadf5..3113b941a 100644 --- a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInAction.kt +++ b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInAction.kt @@ -3,4 +3,5 @@ package com.softartdev.notedelight.presentation.signin sealed interface SignInAction { data object OnSettingsClick : SignInAction data class OnSignInClick(val pass: CharSequence) : SignInAction + data object OnBiometricClick : SignInAction } diff --git a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInResult.kt b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInResult.kt index fb55778bb..ee3eff2f3 100644 --- a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInResult.kt +++ b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInResult.kt @@ -4,5 +4,10 @@ enum class SignInResult(val isError: Boolean = false) { ShowSignInForm, ShowProgress, ShowEmptyPassError(isError = true), - ShowIncorrectPassError(isError = true) + ShowIncorrectPassError(isError = true), + ShowBiometricAvailable, + ShowBiometricInProgress, + ShowBiometricSuccess, + ShowBiometricFailed(isError = true), + ShowBiometricFallbackToPassword } diff --git a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModel.kt b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModel.kt index fde142e82..64c0423d0 100644 --- a/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModel.kt +++ b/core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/signin/SignInViewModel.kt @@ -4,6 +4,8 @@ import androidx.compose.ui.autofill.AutofillManager import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger +import com.softartdev.notedelight.interactor.BiometricAuthResult +import com.softartdev.notedelight.interactor.BiometricAuthService import com.softartdev.notedelight.navigation.AppNavGraph import com.softartdev.notedelight.navigation.Router import com.softartdev.notedelight.usecase.crypt.CheckPasswordUseCase @@ -14,7 +16,8 @@ import kotlinx.coroutines.launch class SignInViewModel( private val checkPasswordUseCase: CheckPasswordUseCase, - private val router: Router + private val router: Router, + private val biometricAuthService: BiometricAuthService ) : ViewModel() { private val logger = Logger.withTag(this@SignInViewModel::class.simpleName.toString()) private val mutableStateFlow: MutableStateFlow = MutableStateFlow( @@ -23,9 +26,20 @@ class SignInViewModel( val stateFlow: StateFlow = mutableStateFlow var autofillManager: AutofillManager? = null + init { + checkBiometricAvailability() + } + fun onAction(action: SignInAction) = when (action) { is SignInAction.OnSettingsClick -> router.navigateClearingBackStack(AppNavGraph.Settings) is SignInAction.OnSignInClick -> signIn(action.pass) + is SignInAction.OnBiometricClick -> signInWithBiometric() + } + + private fun checkBiometricAvailability() = viewModelScope.launch { + if (biometricAuthService.isBiometricAvailable()) { + mutableStateFlow.value = SignInResult.ShowBiometricAvailable + } } private fun signIn(pass: CharSequence) = viewModelScope.launch { @@ -50,4 +64,25 @@ class SignInViewModel( CountingIdlingRes.decrement() } } + + private fun signInWithBiometric() = viewModelScope.launch { + CountingIdlingRes.increment() + mutableStateFlow.value = SignInResult.ShowBiometricInProgress + try { + when (biometricAuthService.authenticate()) { + BiometricAuthResult.Success -> { + router.navigateClearingBackStack(AppNavGraph.Main) + mutableStateFlow.value = SignInResult.ShowBiometricSuccess + } + BiometricAuthResult.Failed -> mutableStateFlow.value = SignInResult.ShowBiometricFailed + BiometricAuthResult.FallbackToPassword -> + mutableStateFlow.value = SignInResult.ShowBiometricFallbackToPassword + } + } catch (error: Throwable) { + logger.e(error) { "Error during biometric sign in" } + mutableStateFlow.value = SignInResult.ShowBiometricFallbackToPassword + } finally { + CountingIdlingRes.decrement() + } + } } diff --git a/core/presentation/src/iosMain/kotlin/com/softartdev/notedelight/interactor/IosBiometricAuthService.kt b/core/presentation/src/iosMain/kotlin/com/softartdev/notedelight/interactor/IosBiometricAuthService.kt new file mode 100644 index 000000000..a133ede96 --- /dev/null +++ b/core/presentation/src/iosMain/kotlin/com/softartdev/notedelight/interactor/IosBiometricAuthService.kt @@ -0,0 +1,42 @@ +package com.softartdev.notedelight.interactor + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCObjectVar +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.coroutines.suspendCancellableCoroutine +import platform.LocalAuthentication.LAContext +import platform.LocalAuthentication.LAPolicyDeviceOwnerAuthenticationWithBiometrics +import platform.LocalAuthentication.LAErrorUserCancel +import platform.LocalAuthentication.LAErrorUserFallback +import kotlin.coroutines.resume + +class IosBiometricAuthService : BiometricAuthService { + + @OptIn(ExperimentalForeignApi::class) + override suspend fun isBiometricAvailable(): Boolean = memScoped { + val authContext = LAContext() + val errorPtr = alloc>() + authContext.canEvaluatePolicy( + policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics, + error = errorPtr.ptr + ) + } + + override suspend fun authenticate(): BiometricAuthResult = suspendCancellableCoroutine { continuation -> + val authContext = LAContext() + authContext.evaluatePolicy( + policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics, + localizedReason = "Authenticate to sign in" + ) { success, error -> + if (!continuation.isActive) return@evaluatePolicy + val result = when { + success -> BiometricAuthResult.Success + error?.code?.toInt() == LAErrorUserFallback || error?.code?.toInt() == LAErrorUserCancel -> + BiometricAuthResult.FallbackToPassword + else -> BiometricAuthResult.Failed + } + continuation.resume(result) + } + } +} diff --git a/core/presentation/src/jvmMain/kotlin/com/softartdev/notedelight/interactor/JvmBiometricAuthService.kt b/core/presentation/src/jvmMain/kotlin/com/softartdev/notedelight/interactor/JvmBiometricAuthService.kt new file mode 100644 index 000000000..54940111a --- /dev/null +++ b/core/presentation/src/jvmMain/kotlin/com/softartdev/notedelight/interactor/JvmBiometricAuthService.kt @@ -0,0 +1,7 @@ +package com.softartdev.notedelight.interactor + +class JvmBiometricAuthService : BiometricAuthService { + override suspend fun isBiometricAvailable(): Boolean = false + + override suspend fun authenticate(): BiometricAuthResult = BiometricAuthResult.FallbackToPassword +} diff --git a/core/presentation/src/wasmJsMain/kotlin/com/softartdev/notedelight/interactor/WasmJsBiometricAuthService.kt b/core/presentation/src/wasmJsMain/kotlin/com/softartdev/notedelight/interactor/WasmJsBiometricAuthService.kt new file mode 100644 index 000000000..84a57497c --- /dev/null +++ b/core/presentation/src/wasmJsMain/kotlin/com/softartdev/notedelight/interactor/WasmJsBiometricAuthService.kt @@ -0,0 +1,7 @@ +package com.softartdev.notedelight.interactor + +class WasmJsBiometricAuthService : BiometricAuthService { + override suspend fun isBiometricAvailable(): Boolean = false + + override suspend fun authenticate(): BiometricAuthResult = BiometricAuthResult.FallbackToPassword +} diff --git a/core/ui/src/androidMain/kotlin/com/softartdev/notedelight/di/sharedModules.android.kt b/core/ui/src/androidMain/kotlin/com/softartdev/notedelight/di/sharedModules.android.kt index 78455f5b6..52c78da89 100644 --- a/core/ui/src/androidMain/kotlin/com/softartdev/notedelight/di/sharedModules.android.kt +++ b/core/ui/src/androidMain/kotlin/com/softartdev/notedelight/di/sharedModules.android.kt @@ -1,6 +1,8 @@ package com.softartdev.notedelight.di import android.content.Context +import com.softartdev.notedelight.interactor.AndroidBiometricAuthService +import com.softartdev.notedelight.interactor.BiometricAuthService import com.softartdev.notedelight.repository.AndroidFileRepo import com.softartdev.notedelight.repository.AndroidSafeRepo import com.softartdev.notedelight.repository.FileRepo @@ -20,3 +22,6 @@ actual val repoModule: Module = module { actual fun Module.factoryOfAppVersionUseCase(): KoinDefinition = factoryOf(constructor = ::AppVersionUseCase) + +actual fun Module.singleOfBiometricAuthService(): KoinDefinition = + factoryOf(constructor = ::AndroidBiometricAuthService) diff --git a/core/ui/src/commonMain/composeResources/values-ru/strings.xml b/core/ui/src/commonMain/composeResources/values-ru/strings.xml index a75167cb1..7d8848a41 100644 --- a/core/ui/src/commonMain/composeResources/values-ru/strings.xml +++ b/core/ui/src/commonMain/composeResources/values-ru/strings.xml @@ -16,6 +16,7 @@ Сохранить изменения? Заметка пуста. Введите данные. Войти + Войти по биометрии Безопасность Включить шифрование Установить пароль @@ -33,6 +34,7 @@ Введите новый пароль Повторите новый пароль Неверный пароль + Сбой биометрии. Используйте пароль. Пароль не должен быть пустым Пароли не совпадают Пока нет заметок diff --git a/core/ui/src/commonMain/composeResources/values/strings.xml b/core/ui/src/commonMain/composeResources/values/strings.xml index d8fa04fa2..b123ae4a7 100644 --- a/core/ui/src/commonMain/composeResources/values/strings.xml +++ b/core/ui/src/commonMain/composeResources/values/strings.xml @@ -16,6 +16,7 @@ Do you want to save the changes? The note is empty. Enter the data. Sign in + Sign in with biometrics Security Enable encryption Set password @@ -33,6 +34,7 @@ Enter new password Repeat new password Incorrect password + Biometric auth failed. Use password. Password must not be empty Passwords do not match No notes yet diff --git a/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/di/sharedModules.kt b/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/di/sharedModules.kt index f02a0a16b..cb0ac7ba1 100644 --- a/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/di/sharedModules.kt +++ b/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/di/sharedModules.kt @@ -1,6 +1,7 @@ package com.softartdev.notedelight.di import com.softartdev.notedelight.db.NoteDAO +import com.softartdev.notedelight.interactor.BiometricAuthService import com.softartdev.notedelight.presentation.files.FilesViewModel import com.softartdev.notedelight.presentation.main.MainViewModel import com.softartdev.notedelight.presentation.note.DeleteViewModel @@ -49,6 +50,7 @@ val daoModule: Module = module { } val useCaseModule: Module = module { + singleOfBiometricAuthService() factoryOf(::ChangePasswordUseCase) factoryOf(::CheckPasswordUseCase) factoryOf(::CheckSqlCipherVersionUseCase) @@ -82,3 +84,4 @@ val viewModelModule: Module = module { } expect fun Module.factoryOfAppVersionUseCase(): KoinDefinition +expect fun Module.singleOfBiometricAuthService(): KoinDefinition diff --git a/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/ui/signin/SignInScreen.kt b/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/ui/signin/SignInScreen.kt index e6fc9572f..a6e6e422c 100644 --- a/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/ui/signin/SignInScreen.kt +++ b/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/ui/signin/SignInScreen.kt @@ -34,6 +34,7 @@ import com.softartdev.notedelight.presentation.signin.SignInResult import com.softartdev.notedelight.presentation.signin.SignInViewModel import com.softartdev.notedelight.ui.PasswordField import com.softartdev.notedelight.ui.TooltipIconButton +import com.softartdev.notedelight.util.SIGN_IN_BIOMETRIC_BUTTON_TAG import com.softartdev.notedelight.util.SIGN_IN_BUTTON_TAG import com.softartdev.notedelight.util.SIGN_IN_PASSWORD_FIELD_TAG import com.softartdev.notedelight.util.SIGN_IN_PASSWORD_LABEL_TAG @@ -41,11 +42,13 @@ import com.softartdev.notedelight.util.SIGN_IN_PASSWORD_VISIBILITY_TAG import com.softartdev.notedelight.util.SIGN_IN_SETTINGS_BUTTON_TAG import notedelight.core.ui.generated.resources.Res import notedelight.core.ui.generated.resources.app_name +import notedelight.core.ui.generated.resources.biometric_auth_failed import notedelight.core.ui.generated.resources.empty_password import notedelight.core.ui.generated.resources.enter_password import notedelight.core.ui.generated.resources.incorrect_password import notedelight.core.ui.generated.resources.settings import notedelight.core.ui.generated.resources.sign_in +import notedelight.core.ui.generated.resources.sign_in_with_biometrics import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource @@ -58,11 +61,20 @@ fun SignInScreen(signInViewModel: SignInViewModel) { signInViewModel.autofillManager = autofillManager } SignInScreenBody( - showLoading = signInResultState.value == SignInResult.ShowProgress, + showLoading = signInResultState.value == SignInResult.ShowProgress || + signInResultState.value == SignInResult.ShowBiometricInProgress, + showBiometricButton = signInResultState.value in setOf( + SignInResult.ShowBiometricAvailable, + SignInResult.ShowBiometricInProgress, + SignInResult.ShowBiometricSuccess, + SignInResult.ShowBiometricFailed, + SignInResult.ShowBiometricFallbackToPassword + ), passwordState = passwordState, labelResource = when (signInResultState.value) { SignInResult.ShowEmptyPassError -> Res.string.empty_password SignInResult.ShowIncorrectPassError -> Res.string.incorrect_password + SignInResult.ShowBiometricFailed -> Res.string.biometric_auth_failed else -> Res.string.enter_password }, isError = signInResultState.value.isError, @@ -74,6 +86,7 @@ fun SignInScreen(signInViewModel: SignInViewModel) { @Composable fun SignInScreenBody( showLoading: Boolean = true, + showBiometricButton: Boolean = false, passwordState: MutableState = mutableStateOf("password"), labelResource: StringResource = Res.string.enter_password, isError: Boolean = false, @@ -117,10 +130,19 @@ fun SignInScreenBody( .padding(top = 24.dp), onClick = { onAction(SignInAction.OnSignInClick(passwordState.value)) }, ) { Text(text = stringResource(Res.string.sign_in)) } + if (showBiometricButton) { + Button( + modifier = Modifier + .testTag(SIGN_IN_BIOMETRIC_BUTTON_TAG) + .fillMaxWidth() + .padding(top = 12.dp), + onClick = { onAction(SignInAction.OnBiometricClick) }, + ) { Text(text = stringResource(Res.string.sign_in_with_biometrics)) } + } } } } @Preview @Composable -fun PreviewSignInScreen() = SignInScreenBody() \ No newline at end of file +fun PreviewSignInScreen() = SignInScreenBody() diff --git a/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/util/TestTags.kt b/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/util/TestTags.kt index d44e30a2a..76e8f38dc 100644 --- a/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/util/TestTags.kt +++ b/core/ui/src/commonMain/kotlin/com/softartdev/notedelight/util/TestTags.kt @@ -8,6 +8,7 @@ const val SIGN_IN_PASSWORD_VISIBILITY_TAG = "SIGN_IN_PASSWORD_FIELD_VISIBILITY_T const val SIGN_IN_PASSWORD_FIELD_TAG = "SIGN_IN_PASSWORD_FIELD_TAG" const val SIGN_IN_SETTINGS_BUTTON_TAG = "SIGN_IN_SETTINGS_BUTTON_TAG" const val SIGN_IN_BUTTON_TAG = "SIGN_IN_BUTTON_TAG" +const val SIGN_IN_BIOMETRIC_BUTTON_TAG = "SIGN_IN_BIOMETRIC_BUTTON_TAG" /** * Test tags for [com.softartdev.notedelight.ui.dialog.security.EnterPasswordDialog] diff --git a/core/ui/src/iosMain/kotlin/com/softartdev/notedelight/di/sharedModules.ios.kt b/core/ui/src/iosMain/kotlin/com/softartdev/notedelight/di/sharedModules.ios.kt index b863a0ca3..63dbf23ff 100644 --- a/core/ui/src/iosMain/kotlin/com/softartdev/notedelight/di/sharedModules.ios.kt +++ b/core/ui/src/iosMain/kotlin/com/softartdev/notedelight/di/sharedModules.ios.kt @@ -1,5 +1,7 @@ package com.softartdev.notedelight.di +import com.softartdev.notedelight.interactor.BiometricAuthService +import com.softartdev.notedelight.interactor.IosBiometricAuthService import com.softartdev.notedelight.repository.FileRepo import com.softartdev.notedelight.repository.IosFileRepo import com.softartdev.notedelight.repository.IosSafeRepo @@ -20,3 +22,6 @@ actual val repoModule: Module = module { actual fun Module.factoryOfAppVersionUseCase(): KoinDefinition = factoryOf( constructor = ::AppVersionUseCase ) + +actual fun Module.singleOfBiometricAuthService(): KoinDefinition = + factoryOf(constructor = ::IosBiometricAuthService) diff --git a/core/ui/src/jvmMain/kotlin/com/softartdev/notedelight/di/sharedModules.jvm.kt b/core/ui/src/jvmMain/kotlin/com/softartdev/notedelight/di/sharedModules.jvm.kt index 97070dd8d..dfff93396 100644 --- a/core/ui/src/jvmMain/kotlin/com/softartdev/notedelight/di/sharedModules.jvm.kt +++ b/core/ui/src/jvmMain/kotlin/com/softartdev/notedelight/di/sharedModules.jvm.kt @@ -1,5 +1,7 @@ package com.softartdev.notedelight.di +import com.softartdev.notedelight.interactor.BiometricAuthService +import com.softartdev.notedelight.interactor.JvmBiometricAuthService import com.softartdev.notedelight.repository.FileRepo import com.softartdev.notedelight.repository.JvmFileRepo import com.softartdev.notedelight.repository.JvmSafeRepo @@ -20,3 +22,7 @@ actual val repoModule: Module = module { actual fun Module.factoryOfAppVersionUseCase(): KoinDefinition = factoryOf( constructor = ::AppVersionUseCase ) + +actual fun Module.singleOfBiometricAuthService(): KoinDefinition = factoryOf( + constructor = ::JvmBiometricAuthService +) diff --git a/core/ui/src/wasmJsMain/kotlin/com/softartdev/notedelight/di/sharedModules.wasmJs.kt b/core/ui/src/wasmJsMain/kotlin/com/softartdev/notedelight/di/sharedModules.wasmJs.kt index cabf9cbc2..7040e3652 100644 --- a/core/ui/src/wasmJsMain/kotlin/com/softartdev/notedelight/di/sharedModules.wasmJs.kt +++ b/core/ui/src/wasmJsMain/kotlin/com/softartdev/notedelight/di/sharedModules.wasmJs.kt @@ -1,5 +1,7 @@ package com.softartdev.notedelight.di +import com.softartdev.notedelight.interactor.BiometricAuthService +import com.softartdev.notedelight.interactor.WasmJsBiometricAuthService import com.softartdev.notedelight.repository.FileRepo import com.softartdev.notedelight.repository.SafeRepo import com.softartdev.notedelight.repository.WasmJsFileRepo @@ -20,3 +22,7 @@ actual val repoModule: Module = module { actual fun Module.factoryOfAppVersionUseCase(): KoinDefinition = factoryOf( constructor = ::AppVersionUseCase ) + +actual fun Module.singleOfBiometricAuthService(): KoinDefinition = factoryOf( + constructor = ::WasmJsBiometricAuthService +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ceac655ad..c96fbc9ec 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ composeMaterial3 = "1.9.0" composeMaterialIconsExtended = "1.7.3" composeMaterialAdaptive = "1.2.0" androidxAppcompat = "1.7.1" +androidxBiometric = "1.4.0-alpha04" androidxViewModel = "2.10.0" androidxNavigation = "2.9.2" androidxNavigationEvent = "1.0.1" @@ -120,6 +121,7 @@ compose-adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:a compose-adaptive-navigation = { module = "org.jetbrains.compose.material3.adaptive:adaptive-navigation", version.ref = "composeMaterialAdaptive" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidxAppcompat" } +androidx-biometric = { module = "androidx.biometric:biometric", version.ref = "androidxBiometric" } androidx-navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidxNavigation" } androidx-navigationevent-compose = { module = "org.jetbrains.androidx.navigationevent:navigationevent-compose", version.ref = "androidxNavigationEvent" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidxActivityCompose" }