feat: Add biometric unlock functionality with saved password retrieval in UnlockScreen
This commit is contained in:
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user