feat: Add biometric authentication toggle in ProfileScreen with state management
This commit is contained in:
@@ -735,7 +735,7 @@ fun UnlockScreen(
|
|||||||
onUnlocking = { isUnlocking = it },
|
onUnlocking = { isUnlocking = it },
|
||||||
onError = { error = it },
|
onError = { error = it },
|
||||||
onSuccess = { decryptedAccount ->
|
onSuccess = { decryptedAccount ->
|
||||||
// Если биометрия доступна и включена, сохраняем пароль
|
// If biometric is enabled, save password
|
||||||
if (biometricAvailable is BiometricAvailability.Available &&
|
if (biometricAvailable is BiometricAvailability.Available &&
|
||||||
isBiometricEnabled && activity != null) {
|
isBiometricEnabled && activity != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -750,8 +750,8 @@ fun UnlockScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError = { /* Игнорируем ошибки при сохранении */ },
|
onError = { /* Ignore save errors */ },
|
||||||
onCancel = { /* Пользователь отменил */ }
|
onCancel = { /* User cancelled */ }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -792,149 +792,6 @@ fun UnlockScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
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
|
// Create New Account button
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = visible && !isDropdownExpanded,
|
visible = visible && !isDropdownExpanded,
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.delay
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -132,6 +137,21 @@ fun ProfileScreen(
|
|||||||
onNavigateToLogs: () -> Unit = {},
|
onNavigateToLogs: () -> Unit = {},
|
||||||
viewModel: ProfileViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
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 backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
|
||||||
val surfaceColor = if (isDarkTheme) Color(0xFF2C2C2E) else Color(0xFFF2F2F7)
|
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
|
// Show success toast and update local profile
|
||||||
LaunchedEffect(profileState.saveSuccess) {
|
LaunchedEffect(profileState.saveSuccess) {
|
||||||
if (profileState.saveSuccess) {
|
if (profileState.saveSuccess) {
|
||||||
@@ -305,9 +322,42 @@ fun ProfileScreen(
|
|||||||
icon = Icons.Outlined.Lock,
|
icon = Icons.Outlined.Lock,
|
||||||
title = "Safety",
|
title = "Safety",
|
||||||
onClick = onNavigateToSafety,
|
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))
|
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
|
@Composable
|
||||||
private fun TelegramLogoutItem(
|
private fun TelegramLogoutItem(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
|||||||
Reference in New Issue
Block a user