feat(wallet): require biometric/PIN auth before transaction signing
Use BIOMETRIC_WEAK | DEVICE_CREDENTIAL to support: - Fingerprint/face → biometric prompt - PIN only → PIN prompt - No auth set up → allow through (dont block tx) Auth fires when user taps Send on confirmation screen, before tx is built/signed/submitted. On failure/cancel, user stays on confirmation screen.
This commit is contained in:
parent
2b93236229
commit
d975d7d761
1 changed files with 79 additions and 42 deletions
|
|
@ -16,7 +16,12 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
|||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Helper class for biometric authentication.
|
||||
* Helper class for biometric authentication at transaction signing.
|
||||
*
|
||||
* Uses BIOMETRIC_WEAK | DEVICE_CREDENTIAL to support:
|
||||
* - Fingerprint/face → biometric prompt
|
||||
* - PIN only → PIN prompt
|
||||
* - No auth set up → skips auth (doesn't block transactions)
|
||||
*/
|
||||
class BiometricAuthenticator @Inject constructor() {
|
||||
|
||||
|
|
@ -26,63 +31,95 @@ class BiometricAuthenticator @Inject constructor() {
|
|||
data object Cancelled : AuthResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any authentication method is available.
|
||||
* Returns true if biometric OR device credential (PIN/pattern/password) is available.
|
||||
*/
|
||||
fun canAuthenticate(context: Context): Boolean {
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
return biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
val result = biometricManager.canAuthenticate(
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
)
|
||||
return result == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device has any form of security (biometric, PIN, pattern, password).
|
||||
* If false, authentication will be skipped to avoid blocking transactions.
|
||||
*/
|
||||
fun isDeviceSecured(context: Context): Boolean {
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
// Check both weak biometric and device credential
|
||||
val weakResult = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
|
||||
val credentialResult = biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
|
||||
return weakResult == BiometricManager.BIOMETRIC_SUCCESS ||
|
||||
credentialResult == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate the user before a sensitive action (e.g., signing a transaction).
|
||||
*
|
||||
* - If device has biometric → shows biometric prompt
|
||||
* - If device has only PIN/pattern/password → shows device credential prompt
|
||||
* - If device has no security → returns Success immediately (don't block the tx)
|
||||
*/
|
||||
suspend fun authenticate(
|
||||
activity: FragmentActivity,
|
||||
title: String = "Authenticate",
|
||||
subtitle: String = "Confirm your identity to continue",
|
||||
): AuthResult = suspendCancellableCoroutine { continuation ->
|
||||
val executor = ContextCompat.getMainExecutor(activity)
|
||||
title: String = "Confirm Payment",
|
||||
subtitle: String = "Authenticate to send ADA",
|
||||
): AuthResult {
|
||||
// If device has no security set up, allow through
|
||||
if (!isDeviceSecured(activity)) {
|
||||
return AuthResult.Success
|
||||
}
|
||||
|
||||
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(AuthResult.Success)
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
val executor = ContextCompat.getMainExecutor(activity)
|
||||
|
||||
val callback = object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(AuthResult.Success)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
if (continuation.isActive) {
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_USER_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_CANCELED -> {
|
||||
continuation.resume(AuthResult.Cancelled)
|
||||
}
|
||||
else -> {
|
||||
continuation.resume(AuthResult.Error(errorCode, errString.toString()))
|
||||
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
||||
if (continuation.isActive) {
|
||||
when (errorCode) {
|
||||
BiometricPrompt.ERROR_USER_CANCELED,
|
||||
BiometricPrompt.ERROR_NEGATIVE_BUTTON,
|
||||
BiometricPrompt.ERROR_CANCELED -> {
|
||||
continuation.resume(AuthResult.Cancelled)
|
||||
}
|
||||
else -> {
|
||||
continuation.resume(AuthResult.Error(errorCode, errString.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
// User can retry, don't complete the continuation
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAuthenticationFailed() {
|
||||
// User can retry
|
||||
val biometricPrompt = BiometricPrompt(activity, executor, callback)
|
||||
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_WEAK or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
.build()
|
||||
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
biometricPrompt.cancelAuthentication()
|
||||
}
|
||||
}
|
||||
|
||||
val biometricPrompt = BiometricPrompt(activity, executor, callback)
|
||||
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(title)
|
||||
.setSubtitle(subtitle)
|
||||
.setAllowedAuthenticators(
|
||||
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
)
|
||||
.build()
|
||||
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
biometricPrompt.cancelAuthentication()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue