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 isBiometricEnabled by remember { mutableStateOf(false) }
var savedPasswordsMap by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
var dataLoaded by remember { mutableStateOf(false) }
// Account selection state
var accounts by remember { mutableStateOf<List<AccountItem>>(emptyList()) }
@@ -228,17 +230,46 @@ fun UnlockScreen(
biometricAvailable = biometricManager.isBiometricAvailable()
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 enabled: $isBiometricEnabled")
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 &&
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) {
// Небольшая задержка для анимации UI
delay(500)
@@ -705,79 +736,133 @@ fun UnlockScreen(
enter = fadeIn(tween(400, delayMillis = 500))
) {
Column {
Button(
onClick = {
if (selectedAccount == null) {
error = "Please select an account"
return@Button
}
if (password.isEmpty()) {
error = "Please enter your password"
return@Button
}
// Check if biometric unlock is available for selected account
val canUseBiometric = remember(selectedAccount, isBiometricEnabled, biometricAvailable) {
selectedAccount != null &&
isBiometricEnabled &&
biometricAvailable is BiometricAvailability.Available &&
activity != null
}
scope.launch {
performUnlock(
selectedAccount = selectedAccount,
password = password,
accountManager = accountManager,
onUnlocking = { isUnlocking = it },
onError = { error = it },
onSuccess = { decryptedAccount ->
// If biometric is enabled and password not saved yet, save password
if (biometricAvailable is BiometricAvailability.Available &&
isBiometricEnabled && activity != null) {
scope.launch {
// Check if password is already saved
val hasPassword = biometricPrefs.hasEncryptedPassword(decryptedAccount.publicKey)
if (!hasPassword) {
biometricManager.encryptPassword(
activity = activity,
password = password,
onSuccess = { encryptedPassword ->
scope.launch {
biometricPrefs.saveEncryptedPassword(
decryptedAccount.publicKey,
encryptedPassword
)
}
},
onError = { /* Ignore save errors */ },
onCancel = { /* User cancelled */ }
val hasSavedPassword = remember(selectedAccount, savedPasswordsMap) {
selectedAccount?.let { savedPasswordsMap.containsKey(it.publicKey) } ?: false
}
val showBiometricButton = canUseBiometric && hasSavedPassword
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Password Unlock Button
Button(
onClick = {
if (selectedAccount == null) {
error = "Please select an account"
return@Button
}
if (password.isEmpty()) {
error = "Please enter your password"
return@Button
}
scope.launch {
performUnlock(
selectedAccount = selectedAccount,
password = password,
accountManager = accountManager,
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 = selectedAccount != null && password.isNotEmpty() && !isUnlocking,
modifier = Modifier.fillMaxWidth().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)
},
enabled = !isUnlocking,
modifier = Modifier
.width(56.dp)
.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),
contentPadding = PaddingValues(0.dp)
) {
Icon(
imageVector = TablerIcons.Fingerprint,
contentDescription = "Unlock with biometric",
modifier = Modifier.size(28.dp)
)
}
}
}
}