feat: Add biometric unlock functionality with saved password retrieval in UnlockScreen

This commit is contained in:
k1ngsterr1
2026-01-24 22:53:25 +05:00
parent 9f9af5e2f7
commit a239486860

View File

@@ -189,6 +189,8 @@ fun UnlockScreen(
// Биометрия // Биометрия
var biometricAvailable by remember { mutableStateOf<BiometricAvailability>(BiometricAvailability.NotAvailable("Проверка...")) } var biometricAvailable by remember { mutableStateOf<BiometricAvailability>(BiometricAvailability.NotAvailable("Проверка...")) }
var isBiometricEnabled by remember { mutableStateOf(false) } var isBiometricEnabled by remember { mutableStateOf(false) }
var savedPasswordsMap by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
var dataLoaded by remember { mutableStateOf(false) }
// Account selection state // Account selection state
var accounts by remember { mutableStateOf<List<AccountItem>>(emptyList()) } var accounts by remember { mutableStateOf<List<AccountItem>>(emptyList()) }
@@ -228,17 +230,46 @@ fun UnlockScreen(
biometricAvailable = biometricManager.isBiometricAvailable() biometricAvailable = biometricManager.isBiometricAvailable()
isBiometricEnabled = biometricPrefs.isBiometricEnabled.first() isBiometricEnabled = biometricPrefs.isBiometricEnabled.first()
// Загружаем сохранённые пароли для всех аккаунтов
val passwords = mutableMapOf<String, String>()
accounts.forEach { account ->
val encryptedPwd = biometricPrefs.getEncryptedPassword(account.publicKey)
if (encryptedPwd != null) {
passwords[account.publicKey] = encryptedPwd
}
}
savedPasswordsMap = passwords
android.util.Log.d("UnlockScreen", "🔐 Biometric available: $biometricAvailable") android.util.Log.d("UnlockScreen", "🔐 Biometric available: $biometricAvailable")
android.util.Log.d("UnlockScreen", "🔐 Biometric enabled: $isBiometricEnabled") android.util.Log.d("UnlockScreen", "🔐 Biometric enabled: $isBiometricEnabled")
android.util.Log.d("UnlockScreen", "🔐 Activity: $activity") android.util.Log.d("UnlockScreen", "🔐 Activity: $activity")
android.util.Log.d("UnlockScreen", "🔐 Selected account: ${selectedAccount?.publicKey}")
android.util.Log.d("UnlockScreen", "🔐 Saved passwords count: ${passwords.size}")
android.util.Log.d("UnlockScreen", "🔐 Has password for selected: ${selectedAccount?.let { passwords.containsKey(it.publicKey) }}")
dataLoaded = true
} }
// Автоматически пытаемся разблокировать через биометрию при выборе аккаунта // Автоматически пытаемся разблокировать через биометрию при выборе аккаунта
LaunchedEffect(selectedAccount, isBiometricEnabled) { LaunchedEffect(selectedAccount, isBiometricEnabled, savedPasswordsMap, dataLoaded) {
android.util.Log.d("UnlockScreen", "🚀 LaunchedEffect triggered")
android.util.Log.d("UnlockScreen", "🚀 dataLoaded: $dataLoaded")
android.util.Log.d("UnlockScreen", "🚀 selectedAccount: ${selectedAccount?.publicKey}")
android.util.Log.d("UnlockScreen", "🚀 isBiometricEnabled: $isBiometricEnabled")
android.util.Log.d("UnlockScreen", "🚀 biometricAvailable: $biometricAvailable")
android.util.Log.d("UnlockScreen", "🚀 activity: $activity")
if (!dataLoaded) {
android.util.Log.d("UnlockScreen", "⏸️ Data not loaded yet, waiting...")
return@LaunchedEffect
}
if (selectedAccount != null && isBiometricEnabled && activity != null && if (selectedAccount != null && isBiometricEnabled && activity != null &&
biometricAvailable is BiometricAvailability.Available) { biometricAvailable is BiometricAvailability.Available) {
val encryptedPassword = biometricPrefs.getEncryptedPassword(selectedAccount!!.publicKey) val encryptedPassword = savedPasswordsMap[selectedAccount!!.publicKey]
android.util.Log.d("UnlockScreen", "🔑 Encrypted password found: ${encryptedPassword != null}")
if (encryptedPassword != null) { if (encryptedPassword != null) {
// Небольшая задержка для анимации UI // Небольшая задержка для анимации UI
delay(500) delay(500)
@@ -705,79 +736,133 @@ fun UnlockScreen(
enter = fadeIn(tween(400, delayMillis = 500)) enter = fadeIn(tween(400, delayMillis = 500))
) { ) {
Column { Column {
Button( // Check if biometric unlock is available for selected account
onClick = { val canUseBiometric = remember(selectedAccount, isBiometricEnabled, biometricAvailable) {
if (selectedAccount == null) { selectedAccount != null &&
error = "Please select an account" isBiometricEnabled &&
return@Button biometricAvailable is BiometricAvailability.Available &&
} activity != null
if (password.isEmpty()) { }
error = "Please enter your password"
return@Button
}
scope.launch { val hasSavedPassword = remember(selectedAccount, savedPasswordsMap) {
performUnlock( selectedAccount?.let { savedPasswordsMap.containsKey(it.publicKey) } ?: false
selectedAccount = selectedAccount, }
password = password,
accountManager = accountManager, val showBiometricButton = canUseBiometric && hasSavedPassword
onUnlocking = { isUnlocking = it },
onError = { error = it }, Row(
onSuccess = { decryptedAccount -> modifier = Modifier.fillMaxWidth(),
// If biometric is enabled and password not saved yet, save password horizontalArrangement = Arrangement.spacedBy(12.dp)
if (biometricAvailable is BiometricAvailability.Available && ) {
isBiometricEnabled && activity != null) { // Password Unlock Button
scope.launch { Button(
// Check if password is already saved onClick = {
val hasPassword = biometricPrefs.hasEncryptedPassword(decryptedAccount.publicKey) if (selectedAccount == null) {
if (!hasPassword) { error = "Please select an account"
biometricManager.encryptPassword( return@Button
activity = activity, }
password = password, if (password.isEmpty()) {
onSuccess = { encryptedPassword -> error = "Please enter your password"
scope.launch { return@Button
biometricPrefs.saveEncryptedPassword( }
decryptedAccount.publicKey,
encryptedPassword scope.launch {
) performUnlock(
} selectedAccount = selectedAccount,
}, password = password,
onError = { /* Ignore save errors */ }, accountManager = accountManager,
onCancel = { /* User cancelled */ } onUnlocking = { isUnlocking = it },
onError = { error = it },
onSuccess = { decryptedAccount ->
onUnlocked(decryptedAccount)
}
)
}
},
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
modifier = Modifier
.weight(1f)
.height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp)
) {
if (isUnlocking) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Icon(
imageVector = TablerIcons.LockOpen,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
}
}
// Biometric Unlock Button
if (showBiometricButton) {
Button(
onClick = {
if (selectedAccount == null || activity == null) return@Button
val encryptedPassword = savedPasswordsMap[selectedAccount!!.publicKey]
if (encryptedPassword != null) {
biometricManager.decryptPassword(
activity = activity,
encryptedData = encryptedPassword,
onSuccess = { 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 = {
// User cancelled
} }
} )
onUnlocked(decryptedAccount)
} }
) },
} enabled = !isUnlocking,
}, modifier = Modifier
enabled = selectedAccount != null && password.isNotEmpty() && !isUnlocking, .width(56.dp)
modifier = Modifier.fillMaxWidth().height(56.dp), .height(56.dp),
colors = colors =
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
containerColor = PrimaryBlue, containerColor = PrimaryBlue,
contentColor = Color.White, contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f), disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f) disabledContentColor = Color.White.copy(alpha = 0.5f)
), ),
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp),
) { contentPadding = PaddingValues(0.dp)
if (isUnlocking) { ) {
CircularProgressIndicator( Icon(
modifier = Modifier.size(24.dp), imageVector = TablerIcons.Fingerprint,
color = Color.White, contentDescription = "Unlock with biometric",
strokeWidth = 2.dp modifier = Modifier.size(28.dp)
) )
} else { }
Icon(
imageVector = TablerIcons.LockOpen,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Unlock", fontSize = 16.sp, fontWeight = FontWeight.SemiBold)
} }
} }
} }