feat: Add biometric authentication toggle in ProfileScreen with state management
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user