feat: Add biometric authentication toggle in ProfileScreen with state management

This commit is contained in:
k1ngsterr1
2026-01-22 15:00:33 +05:00
parent 1f6fc01a54
commit c7443a7ed7
2 changed files with 103 additions and 150 deletions

View File

@@ -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,

View File

@@ -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>(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,
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,