mobiler 0.13.0

Build mobile apps in Rust — one core, native UI on Android, iOS, and the web (CLI)
package {{PACKAGE}}

import {{PACKAGE_SHARED_TYPES}}.PluginResponse

import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume

// Biometric authentication (free, bundled). Presents the system Face/fingerprint sheet via
// androidx.biometric — which requires a FragmentActivity host (the Mobiler shell's MainActivity
// is one). `input` is the prompt title (optional). Returns ok:true on success; ok:false with a
// reason on cancel / failure / no enrolled biometric / no hardware.
//
// Gate a sensitive action in your Rust core by checking the result before proceeding; pair with
// the `securestore` plugin to protect stored secrets.
class BiometricPlugin(private val application: android.app.Application) : MobilerPlugin {
    override suspend fun handle(op: String, input: String): PluginResponse {
        if (op != "authenticate") return PluginResponse(false, "unknown op '$op'")
        val activity = MobilerActivity.current?.get() as? FragmentActivity
            ?: return PluginResponse(false, "no FragmentActivity")

        // Surface a clear reason when auth can't even be attempted.
        val canAuth = BiometricManager.from(application).canAuthenticate(
            BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
        )
        if (canAuth != BiometricManager.BIOMETRIC_SUCCESS) {
            return PluginResponse(false, "biometric unavailable ($canAuth)")
        }

        val title = input.ifEmpty { "Authenticate" }
        return suspendCancellableCoroutine { cont ->
            val executor = ContextCompat.getMainExecutor(application)
            val prompt = BiometricPrompt(activity, executor, object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    if (cont.isActive) cont.resume(PluginResponse(true, "ok"))
                }
                override fun onAuthenticationError(code: Int, msg: CharSequence) {
                    if (cont.isActive) cont.resume(PluginResponse(false, msg.toString()))
                }
                // onAuthenticationFailed (a single bad finger) is transient — the prompt stays up,
                // so we don't resume here; resume only on terminal success/error.
            })
            val info = BiometricPrompt.PromptInfo.Builder()
                .setTitle(title)
                .setAllowedAuthenticators(
                    BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
                )
                .build()
            // BiometricPrompt must be driven on the main thread.
            activity.runOnUiThread { prompt.authenticate(info) }
        }
    }
}