Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 156 additions & 22 deletions android/src/main/java/com/simplebiometrics/SimpleBiometricsModule.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.simplebiometrics

import android.util.Log
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
Expand All @@ -12,14 +15,58 @@ import com.facebook.react.module.annotations.ReactModule

@ReactModule(name = SimpleBiometricsModule.NAME)
class SimpleBiometricsModule(reactContext: ReactApplicationContext) :
NativeSimpleBiometricsSpec(reactContext) {
NativeSimpleBiometricsSpec(reactContext), LifecycleEventListener {

data class PendingAuthRequest(
val promptTitle: String?,
val promptMessage: String?,
val allowDeviceCredentials: Boolean,
val promise: Promise
)

private var pendingAuthRequest: PendingAuthRequest? = null
private var isAuthenticationActive = false
private var isHostResumed = false
private var launchAttemptScheduled = false

init {
reactContext.addLifecycleEventListener(this)
}

override fun getName(): String {
return NAME
}

companion object {
const val NAME = "SimpleBiometrics"
private const val LOG_TAG = "SimpleBiometrics"
}

override fun onHostResume() {
isHostResumed = true
Log.d(LOG_TAG, "onHostResume pending=${pendingAuthRequest != null} active=$isAuthenticationActive scheduled=$launchAttemptScheduled")
launchPendingAuthentication()
}

override fun onHostPause() {
Log.d(LOG_TAG, "onHostPause pending=${pendingAuthRequest != null} active=$isAuthenticationActive")
isHostResumed = false
launchAttemptScheduled = false
}

override fun onHostDestroy() {
Log.d(LOG_TAG, "onHostDestroy pending=${pendingAuthRequest != null} active=$isAuthenticationActive")
isHostResumed = false
isAuthenticationActive = false
}

override fun invalidate() {
reactApplicationContext.removeLifecycleEventListener(this)
pendingAuthRequest = null
isAuthenticationActive = false
isHostResumed = false
launchAttemptScheduled = false
super.invalidate()
}

/**
Expand Down Expand Up @@ -61,44 +108,131 @@ class SimpleBiometricsModule(reactContext: ReactApplicationContext) :
promise: Promise?
) {
runOnUiThread {
if (promise == null) {
Log.d(LOG_TAG, "requestBioAuth ignored because promise was null")
return@runOnUiThread
}

if (pendingAuthRequest != null) {
Log.d(LOG_TAG, "requestBioAuth rejected because another request is already pending")
promise.reject("BIOMETRIC_IN_PROGRESS", "Another biometric authentication request is already in progress")
return@runOnUiThread
}

pendingAuthRequest = PendingAuthRequest(
promptTitle = promptTitle,
promptMessage = promptMessage,
allowDeviceCredentials = allowDeviceCredentials,
promise = promise
)

Log.d(
LOG_TAG,
"requestBioAuth queued hostResumed=$isHostResumed active=$isAuthenticationActive title=${promptTitle ?: ""}"
)

launchPendingAuthentication()
}
}

private fun launchPendingAuthentication() {
runOnUiThread {
val request = pendingAuthRequest ?: return@runOnUiThread
if (isAuthenticationActive) {
Log.d(LOG_TAG, "launchPendingAuthentication skipped because authentication is already active")
return@runOnUiThread
}

if (!isHostResumed) {
Log.d(LOG_TAG, "launchPendingAuthentication skipped because host is not resumed")
return@runOnUiThread
}

val activity = reactApplicationContext.currentActivity as? FragmentActivity ?: return@runOnUiThread

Log.d(
LOG_TAG,
"launchPendingAuthentication activity=${activity::class.java.simpleName} lifecycle=${activity.lifecycle.currentState} finishing=${activity.isFinishing} destroyed=${activity.isDestroyed}"
)

if (activity.isFinishing || activity.isDestroyed) {
Log.d(LOG_TAG, "launchPendingAuthentication aborted because activity is finishing or destroyed")
return@runOnUiThread
}

if (!activity.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
if (!launchAttemptScheduled) {
Log.d(LOG_TAG, "launchPendingAuthentication deferring until decorView post because lifecycle is ${activity.lifecycle.currentState}")
launchAttemptScheduled = true
activity.window?.decorView?.post {
launchAttemptScheduled = false
Log.d(LOG_TAG, "launchPendingAuthentication retrying after decorView post")
launchPendingAuthentication()
} ?: run {
launchAttemptScheduled = false
Log.d(LOG_TAG, "launchPendingAuthentication could not schedule decorView post")
}
}
return@runOnUiThread
}

try {
isAuthenticationActive = true
Log.d(LOG_TAG, "launchPendingAuthentication showing BiometricPrompt allowDeviceCredentials=${request.allowDeviceCredentials}")
val context = reactApplicationContext
val activity = this.reactApplicationContext.currentActivity
val mainExecutor = ContextCompat.getMainExecutor(context)
val authenticationCallback: BiometricPrompt.AuthenticationCallback =
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
promise!!.reject(java.lang.Exception(errString.toString()))
isAuthenticationActive = false
Log.d(
LOG_TAG,
"onAuthenticationError code=$errorCode message=$errString hostResumed=$isHostResumed pending=${pendingAuthRequest != null}"
)
if (errorCode == BiometricPrompt.ERROR_CANCELED && !isHostResumed) {
Log.d(LOG_TAG, "onAuthenticationError preserving pending request for next resume")
return
}

pendingAuthRequest = null
if (errorCode == BiometricPrompt.ERROR_CANCELED) {
request.promise.reject("BIOMETRIC_SYSTEM_CANCELED", errString.toString())
} else {
request.promise.reject(java.lang.Exception(errString.toString()))
}
}

override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
promise!!.resolve(true)
isAuthenticationActive = false
pendingAuthRequest = null
Log.d(LOG_TAG, "onAuthenticationSucceeded")
request.promise.resolve(true)
}
}

if (activity != null) {
val prompt = BiometricPrompt(
activity as FragmentActivity, mainExecutor,
authenticationCallback
)

val authenticators = getAllowedAuthenticators(allowDeviceCredentials)
val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(authenticators)
.setTitle(promptTitle ?: "")
.setSubtitle(promptMessage)
.build()

prompt.authenticate(promptInfo)
} else {
throw java.lang.Exception("null activity")
}
val prompt = BiometricPrompt(
activity,
mainExecutor,
authenticationCallback
)

val authenticators = getAllowedAuthenticators(request.allowDeviceCredentials)
val promptInfo: BiometricPrompt.PromptInfo = BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(authenticators)
.setTitle(request.promptTitle ?: "")
.setSubtitle(request.promptMessage)
.build()

prompt.authenticate(promptInfo)
} catch (e: java.lang.Exception) {
promise!!.reject(e)
isAuthenticationActive = false
pendingAuthRequest = null
Log.d(LOG_TAG, "launchPendingAuthentication failed: ${e.message}", e)
request.promise.reject(e)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-simple-biometrics",
"version": "3.0.2",
"version": "3.0.6",
"description": "simple interface to verify user authenticity using on-device biometrics",
"main": "./lib/module/index.js",
"types": "./lib/typescript/src/index.d.ts",
Expand Down