diff --git a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt index 2a8307353a..e8b29e317d 100644 --- a/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt +++ b/features/wallet/impl/src/main/kotlin/io/element/android/features/wallet/impl/biometric/BiometricAuthenticator.kt @@ -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() - } } }