From c7443a7ed72953f8e5d11b4f15ca2fb7d54ce068 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Thu, 22 Jan 2026 15:00:33 +0500 Subject: [PATCH] feat: Add biometric authentication toggle in ProfileScreen with state management --- .../rosetta/messenger/ui/auth/UnlockScreen.kt | 149 +----------------- .../messenger/ui/settings/ProfileScreen.kt | 104 +++++++++++- 2 files changed, 103 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt index a761213..aaa0ad9 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/auth/UnlockScreen.kt @@ -735,7 +735,7 @@ fun UnlockScreen( onUnlocking = { isUnlocking = it }, onError = { error = it }, onSuccess = { decryptedAccount -> - // Если биометрия доступна и включена, сохраняем пароль + // If biometric is enabled, save password if (biometricAvailable is BiometricAvailability.Available && isBiometricEnabled && activity != null) { scope.launch { @@ -750,8 +750,8 @@ fun UnlockScreen( ) } }, - onError = { /* Игнорируем ошибки при сохранении */ }, - onCancel = { /* Пользователь отменил */ } + onError = { /* Ignore save errors */ }, + onCancel = { /* User cancelled */ } ) } } @@ -792,149 +792,6 @@ fun UnlockScreen( Spacer(modifier = Modifier.height(16.dp)) - // Кнопка для включения/отключения биометрии (вне AnimatedVisibility) - if (biometricAvailable is BiometricAvailability.Available && activity != null && !isDropdownExpanded) { - // Кнопка входа по биометрии (если биометрия включена и пароль сохранен) - var hasSavedPassword by remember { mutableStateOf(false) } - - LaunchedEffect(selectedAccount, isBiometricEnabled) { - hasSavedPassword = if (selectedAccount != null && isBiometricEnabled) { - biometricPrefs.hasEncryptedPassword(selectedAccount!!.publicKey) - } else false - } - - if (isBiometricEnabled && hasSavedPassword) { - // Кнопка для входа по биометрии - OutlinedButton( - onClick = { - selectedAccount?.let { account -> - scope.launch { - val encryptedPassword = biometricPrefs.getEncryptedPassword(account.publicKey) - if (encryptedPassword != null) { - biometricManager.decryptPassword( - activity = activity, - encryptedData = encryptedPassword, - onSuccess = { decryptedPassword -> - password = decryptedPassword - scope.launch { - performUnlock( - selectedAccount = selectedAccount, - password = decryptedPassword, - accountManager = accountManager, - onUnlocking = { isUnlocking = it }, - onError = { error = it }, - onSuccess = { decryptedAccount -> - onUnlocked(decryptedAccount) - } - ) - } - }, - onError = { errorMessage -> - error = "Ошибка биометрии: $errorMessage" - }, - onCancel = { } - ) - } - } - } - }, - modifier = Modifier.fillMaxWidth().height(56.dp), - shape = RoundedCornerShape(12.dp), - border = BorderStroke(1.dp, PrimaryBlue) - ) { - Icon( - imageVector = Icons.Default.Fingerprint, - contentDescription = null, - tint = PrimaryBlue, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Войти по отпечатку", - color = PrimaryBlue, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - } - - // Переключатель биометрии - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .clickable { - scope.launch { - if (isBiometricEnabled) { - // Отключаем биометрию - biometricPrefs.disableBiometric() - selectedAccount?.let { - biometricPrefs.removeEncryptedPassword(it.publicKey) - } - isBiometricEnabled = false - } else { - // Включаем биометрию - нужно сохранить пароль - if (password.isNotEmpty()) { - // Сначала проверим пароль - val account = selectedAccount?.encryptedAccount - if (account != null) { - val decryptedPrivateKey = CryptoManager.decryptWithPassword( - account.encryptedPrivateKey, - password - ) - if (decryptedPrivateKey != null) { - // Пароль верный, сохраняем через биометрию - biometricManager.encryptPassword( - activity = activity, - password = password, - onSuccess = { encryptedPassword -> - scope.launch { - biometricPrefs.enableBiometric() - biometricPrefs.saveEncryptedPassword( - account.publicKey, - encryptedPassword - ) - isBiometricEnabled = true - } - }, - onError = { errorMsg -> - error = "Ошибка сохранения: $errorMsg" - }, - onCancel = { } - ) - } else { - error = "Неверный пароль" - } - } - } else { - error = "Сначала введите пароль" - } - } - } - } - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Default.Fingerprint, - contentDescription = null, - tint = if (isBiometricEnabled) PrimaryBlue else secondaryTextColor, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = if (isBiometricEnabled) "Биометрия включена" else "Включить биометрию", - color = if (isBiometricEnabled) PrimaryBlue else secondaryTextColor, - fontSize = 14.sp - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - } - // Create New Account button AnimatedVisibility( visible = visible && !isDropdownExpanded, diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt index 774eb6f..8dac2b2 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/ProfileScreen.kt @@ -43,7 +43,12 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.fragment.app.FragmentActivity +import com.rosetta.messenger.biometric.BiometricAuthManager +import com.rosetta.messenger.biometric.BiometricAvailability +import com.rosetta.messenger.biometric.BiometricPreferences import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -132,6 +137,21 @@ fun ProfileScreen( onNavigateToLogs: () -> Unit = {}, viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel() ) { + val context = LocalContext.current + val activity = context as? FragmentActivity + val biometricManager = remember { BiometricAuthManager(context) } + val biometricPrefs = remember { BiometricPreferences(context) } + val scope = rememberCoroutineScope() + + // Biometric state + var biometricAvailable by remember { mutableStateOf(BiometricAvailability.NotAvailable("Checking...")) } + var isBiometricEnabled by remember { mutableStateOf(false) } + + // Check biometric availability + LaunchedEffect(Unit) { + biometricAvailable = biometricManager.isBiometricAvailable() + isBiometricEnabled = biometricPrefs.isBiometricEnabled.first() + } // Цвета в зависимости от темы val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7) @@ -194,9 +214,6 @@ fun ProfileScreen( } } - // Context - val context = LocalContext.current - // Show success toast and update local profile LaunchedEffect(profileState.saveSuccess) { if (profileState.saveSuccess) { @@ -305,8 +322,41 @@ fun ProfileScreen( icon = Icons.Outlined.Lock, title = "Safety", onClick = onNavigateToSafety, - isDarkTheme = isDarkTheme + isDarkTheme = isDarkTheme, + showDivider = biometricAvailable is BiometricAvailability.Available ) + + // Biometric toggle (only show if available) + if (biometricAvailable is BiometricAvailability.Available && activity != null) { + TelegramBiometricItem( + isEnabled = isBiometricEnabled, + onToggle = { + scope.launch { + if (isBiometricEnabled) { + // Disable biometric + biometricPrefs.disableBiometric() + biometricPrefs.removeEncryptedPassword(accountPublicKey) + isBiometricEnabled = false + android.widget.Toast.makeText( + context, + "Biometric authentication disabled", + android.widget.Toast.LENGTH_SHORT + ).show() + } else { + // Enable biometric + biometricPrefs.enableBiometric() + isBiometricEnabled = true + android.widget.Toast.makeText( + context, + "Biometric authentication enabled. Your password will be securely saved on next unlock.", + android.widget.Toast.LENGTH_LONG + ).show() + } + } + }, + isDarkTheme = isDarkTheme + ) + } Spacer(modifier = Modifier.height(24.dp)) @@ -831,6 +881,52 @@ private fun TelegramSettingsItem( } } +@Composable +private fun TelegramBiometricItem( + isEnabled: Boolean, + onToggle: () -> Unit, + isDarkTheme: Boolean +) { + val textColor = if (isDarkTheme) Color.White else Color.Black + val iconColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) + val primaryBlue = Color(0xFF007AFF) + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onToggle) + .padding(horizontal = 16.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Outlined.Fingerprint, + contentDescription = null, + tint = iconColor, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(20.dp)) + + Text( + text = "Biometric Authentication", + fontSize = 16.sp, + color = textColor, + modifier = Modifier.weight(1f) + ) + + Switch( + checked = isEnabled, + onCheckedChange = { onToggle() }, + colors = SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = primaryBlue, + uncheckedThumbColor = Color.White, + uncheckedTrackColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0) + ) + ) + } +} + @Composable private fun TelegramLogoutItem( onClick: () -> Unit,