Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,4 @@ captures

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
*/.codex/*
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,13 @@ Get the native Capacitor plugin version.

#### AvailableResult

| Prop | Type |
| ------------------ | ----------------------------------------------------- |
| **`isAvailable`** | <code>boolean</code> |
| **`biometryType`** | <code><a href="#biometrytype">BiometryType</a></code> |
| **`errorCode`** | <code>number</code> |
Result from isAvailable() method indicating biometric authentication availability.

| Prop | Type | Description |
| ---------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`isAvailable`** | <code>boolean</code> | Whether authentication is available (biometric or fallback if useFallback is true) |
| **`authenticationStrength`** | <code><a href="#authenticationstrength">AuthenticationStrength</a></code> | The strength of available authentication method (STRONG, WEAK, or NONE) |
| **`errorCode`** | <code><a href="#biometricautherror">BiometricAuthError</a></code> | Error code from <a href="#biometricautherror">BiometricAuthError</a> enum. Only present when isAvailable is false. Indicates why biometric authentication is not available. |


#### IsAvailableOptions
Expand Down Expand Up @@ -313,6 +315,34 @@ Get the native Capacitor plugin version.
### Enums


#### AuthenticationStrength

| Members | Value | Description |
| ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`NONE`** | <code>0</code> | No authentication available, even if PIN is available but useFallback = false |
| **`STRONG`** | <code>1</code> | Strong authentication: Face ID on iOS, fingerprints on devices that consider fingerprints strong (Android). Note: PIN/pattern/password is NEVER considered STRONG, even when useFallback = true. |
| **`WEAK`** | <code>2</code> | Weak authentication: Face authentication on Android devices that consider face weak, or PIN/pattern/password if useFallback = true (PIN is always WEAK, never STRONG). |


#### BiometricAuthError

| Members | Value | Description |
| ----------------------------- | --------------- | ------------------------------------------------------------------------------------- |
| **`UNKNOWN_ERROR`** | <code>0</code> | Unknown error occurred |
| **`BIOMETRICS_UNAVAILABLE`** | <code>1</code> | Biometrics are unavailable (no hardware or hardware error) Platform: Android, iOS |
| **`USER_LOCKOUT`** | <code>2</code> | User has been locked out due to too many failed attempts Platform: Android, iOS |
| **`BIOMETRICS_NOT_ENROLLED`** | <code>3</code> | No biometrics are enrolled on the device Platform: Android, iOS |
| **`USER_TEMPORARY_LOCKOUT`** | <code>4</code> | User is temporarily locked out (Android: 30 second lockout) Platform: Android |
| **`AUTHENTICATION_FAILED`** | <code>10</code> | Authentication failed (user did not authenticate successfully) Platform: Android, iOS |
| **`APP_CANCEL`** | <code>11</code> | App canceled the authentication (iOS only) Platform: iOS |
| **`INVALID_CONTEXT`** | <code>12</code> | Invalid context (iOS only) Platform: iOS |
| **`NOT_INTERACTIVE`** | <code>13</code> | Authentication was not interactive (iOS only) Platform: iOS |
| **`PASSCODE_NOT_SET`** | <code>14</code> | Passcode/PIN is not set on the device Platform: Android, iOS |
| **`SYSTEM_CANCEL`** | <code>15</code> | System canceled the authentication (e.g., due to screen lock) Platform: Android, iOS |
| **`USER_CANCEL`** | <code>16</code> | User canceled the authentication Platform: Android, iOS |
| **`USER_FALLBACK`** | <code>17</code> | User chose to use fallback authentication method Platform: Android, iOS |


#### BiometryType

| Members | Value |
Expand Down
137 changes: 75 additions & 62 deletions android/src/main/java/ee/forgr/biometric/NativeBiometric.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ public class NativeBiometric extends Plugin {
private static final int IRIS_AUTHENTICATION = 5;
private static final int MULTIPLE = 6;

// AuthenticationStrength enum values
private static final int AUTH_STRENGTH_NONE = 0;
private static final int AUTH_STRENGTH_STRONG = 1;
private static final int AUTH_STRENGTH_WEAK = 2;

private KeyStore keyStore;
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
Expand All @@ -72,82 +77,71 @@ public class NativeBiometric extends Plugin {

private SharedPreferences encryptedSharedPreferences;

private int getAvailableFeature() {
// default to none
BiometricManager biometricManager = BiometricManager.from(getContext());

// Check for biometric capabilities
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
int canAuthenticate = biometricManager.canAuthenticate(authenticators);

if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
// Check specific features
PackageManager pm = getContext().getPackageManager();
boolean hasFinger = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
boolean hasIris = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
hasIris = pm.hasSystemFeature(PackageManager.FEATURE_IRIS);
}

// For face, we rely on BiometricManager since it's more reliable
boolean hasFace = false;
try {
// Try to create a face authentication prompt - if it succeeds, face auth is available
androidx.biometric.BiometricPrompt.PromptInfo promptInfo = new androidx.biometric.BiometricPrompt.PromptInfo.Builder()
.setTitle("Test")
.setNegativeButtonText("Cancel")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build();
hasFace = true;
} catch (Exception e) {
System.out.println("Error creating face authentication prompt: " + e.getMessage());
}

// Determine the type based on available features
if (hasFinger && (hasFace || hasIris)) {
return MULTIPLE;
} else if (hasFinger) {
return FINGERPRINT;
} else if (hasFace) {
return FACE_AUTHENTICATION;
} else if (hasIris) {
return IRIS_AUTHENTICATION;
}
}

return NONE;
}

@PluginMethod
public void isAvailable(PluginCall call) {
JSObject ret = new JSObject();

boolean useFallback = Boolean.TRUE.equals(call.getBoolean("useFallback", false));

BiometricManager biometricManager = BiometricManager.from(getContext());
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
if (useFallback) {
authenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL;
}
int canAuthenticateResult = biometricManager.canAuthenticate(authenticators);
// Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
// > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
// @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)

// Check for strong biometrics first
int strongAuthenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
int strongResult = biometricManager.canAuthenticate(strongAuthenticators);
boolean hasStrongBiometric = (strongResult == BiometricManager.BIOMETRIC_SUCCESS);

// Check for weak biometrics
int weakAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;
int weakResult = biometricManager.canAuthenticate(weakAuthenticators);
boolean hasWeakBiometric = (weakResult == BiometricManager.BIOMETRIC_SUCCESS);

// Check if device has credentials (PIN/pattern/password)
boolean fallbackAvailable = useFallback && this.deviceHasCredentials();
if (useFallback && !fallbackAvailable) {
canAuthenticateResult = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
}

boolean isAvailable = (canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS || fallbackAvailable);
ret.put("isAvailable", isAvailable);
// Determine authentication strength
int authenticationStrength = AUTH_STRENGTH_NONE;
boolean isAvailable = false;

if (hasStrongBiometric) {
// Strong biometric available (fingerprints on devices that consider them strong)
authenticationStrength = AUTH_STRENGTH_STRONG;
isAvailable = true;
} else if (hasWeakBiometric) {
// Only weak biometric available (face on devices that consider it weak)
authenticationStrength = AUTH_STRENGTH_WEAK;
isAvailable = true;
} else if (fallbackAvailable) {
// No biometrics but fallback (PIN/pattern/password) is available
// PIN/pattern/password is ALWAYS considered WEAK, never STRONG
authenticationStrength = AUTH_STRENGTH_WEAK;
isAvailable = true;
}

// Handle error codes when authentication is not available
if (!isAvailable) {
// BiometricManager Error Constants use the same values as BiometricPrompt's Constants. So we can reuse our
int pluginErrorCode = AuthActivity.convertToPluginErrorCode(canAuthenticateResult);
int biometricManagerErrorCode;

// Prefer the error from strong biometric check if it failed
if (strongResult != BiometricManager.BIOMETRIC_SUCCESS) {
biometricManagerErrorCode = strongResult;
} else if (weakResult != BiometricManager.BIOMETRIC_SUCCESS) {
// Otherwise use error from weak biometric check if it failed
biometricManagerErrorCode = weakResult;
} else {
// No biometrics available at all
// BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE indicates that biometric hardware is unavailable
// or cannot be accessed. This constant value may vary across Android versions, so we explicitly
// use the constant rather than assuming its numeric value.
biometricManagerErrorCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
}

// Convert BiometricManager error codes to plugin error codes
int pluginErrorCode = convertBiometricManagerErrorToPluginError(biometricManagerErrorCode);
ret.put("errorCode", pluginErrorCode);
}

ret.put("biometryType", getAvailableFeature());
ret.put("isAvailable", isAvailable);
ret.put("authenticationStrength", authenticationStrength);
call.resolve(ret);
}

Expand Down Expand Up @@ -463,6 +457,25 @@ private boolean deviceHasCredentials() {
return keyguardManager.isDeviceSecure();
}

/**
* Convert BiometricManager error codes to plugin error codes
* BiometricManager constants have different values than BiometricPrompt constants
*/
private int convertBiometricManagerErrorToPluginError(int errorCode) {
switch (errorCode) {
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
case BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED:
return 1; // BIOMETRICS_UNAVAILABLE
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
return 3; // BIOMETRICS_NOT_ENROLLED
case BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED:
return 1; // BIOMETRICS_UNAVAILABLE (security update required, treat as unavailable)
default:
return 0; // UNKNOWN_ERROR
}
}

@PluginMethod
public void getPluginVersion(final PluginCall call) {
try {
Expand Down
18 changes: 7 additions & 11 deletions example-app/simple-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ <h2>Console Output</h2>
</div>

<script type="module">
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
import { NativeBiometric, AuthenticationStrength } from '@capgo/capacitor-native-biometric';
import { SplashScreen } from '@capacitor/splash-screen';

// Override console to show logs
Expand Down Expand Up @@ -169,7 +169,7 @@ <h2>Console Output</h2>
const div = document.createElement('div');
div.className = result.isAvailable ? 'result success' : 'result error';
div.textContent = result.isAvailable
? `✅ Biometrics Available: ${getBiometryName(result.biometryType)}`
? `✅ Biometrics Available: ${getAuthenticationStrengthName(result.authenticationStrength)}`
: `❌ Biometrics Not Available (Error: ${result.errorCode})`;
document.getElementById('other-results').appendChild(div);
} catch (error) {
Expand Down Expand Up @@ -222,17 +222,13 @@ <h2>Console Output</h2>
}
};

function getBiometryName(type) {
function getAuthenticationStrengthName(strength) {
const names = {
0: 'None',
1: 'Touch ID',
2: 'Face ID',
3: 'Fingerprint',
4: 'Face Authentication',
5: 'Iris Authentication',
6: 'Multiple'
[AuthenticationStrength.NONE]: 'None',
[AuthenticationStrength.STRONG]: 'Strong',
[AuthenticationStrength.WEAK]: 'Weak'
};
return names[type] || 'Unknown';
return names[strength] || 'Unknown';
}

// Hide splash screen
Expand Down
33 changes: 19 additions & 14 deletions example-app/src/js/biometric-tester.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
import { NativeBiometric, AuthenticationStrength } from '@capgo/capacitor-native-biometric';
import { SplashScreen } from '@capacitor/splash-screen';

// Simple function-based approach - no Web Components
Expand Down Expand Up @@ -110,6 +110,12 @@ function createBiometricTester() {
<div class="section">
<h2>Biometric Status</h2>
<div id="biometric-status" class="status">Checking...</div>
<div class="form-group" style="margin-bottom: 10px;">
<label style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" id="use-fallback-check" style="width: auto; margin-right: 8px;">
Use Fallback (passcode/PIN)
</label>
</div>
<button class="button" id="check-availability">Check Availability</button>
<div id="biometric-info" class="info" style="display: none;"></div>
</div>
Expand Down Expand Up @@ -167,6 +173,7 @@ function createBiometricTester() {
// Add event listeners
const elements = {
checkAvailability: container.querySelector('#check-availability'),
useFallbackCheck: container.querySelector('#use-fallback-check'),
verifyIdentity: container.querySelector('#verify-identity'),
verifyIdentityFallback: container.querySelector('#verify-identity-fallback'),
setCredentials: container.querySelector('#set-credentials'),
Expand Down Expand Up @@ -227,15 +234,17 @@ function createBiometricTester() {
// Functions
async function checkAvailability() {
try {
addConsoleLog('INFO', ['Checking biometric availability...']);
const result = await NativeBiometric.isAvailable();
const useFallback = elements.useFallbackCheck.checked;
addConsoleLog('INFO', [`Checking biometric availability (useFallback: ${useFallback})...`]);
const result = await NativeBiometric.isAvailable({ useFallback });
addConsoleLog('INFO', ['Availability result:', result]);

if (result.isAvailable) {
elements.biometricStatus.textContent = `✅ Biometrics Available (${getBiometryTypeName(result.biometryType)})`;
const strengthName = getAuthenticationStrengthName(result.authenticationStrength);
elements.biometricStatus.textContent = `✅ Biometrics Available (${strengthName})`;
elements.biometricStatus.className = 'status available';
elements.biometricInfo.style.display = 'block';
elements.biometricInfo.innerHTML = `<strong>Biometry Type:</strong> ${getBiometryTypeName(result.biometryType)} (${result.biometryType})`;
elements.biometricInfo.innerHTML = `<strong>Authentication Strength:</strong> ${strengthName} (${result.authenticationStrength})`;
} else {
elements.biometricStatus.textContent = `❌ Biometrics Not Available (Error: ${result.errorCode || 'Unknown'})`;
elements.biometricStatus.className = 'status unavailable';
Expand Down Expand Up @@ -367,17 +376,13 @@ function createBiometricTester() {
}
}

function getBiometryTypeName(type) {
function getAuthenticationStrengthName(strength) {
const names = {
0: 'None',
1: 'Touch ID',
2: 'Face ID',
3: 'Fingerprint',
4: 'Face Authentication',
5: 'Iris Authentication',
6: 'Multiple'
[AuthenticationStrength.NONE]: 'None',
[AuthenticationStrength.STRONG]: 'Strong',
[AuthenticationStrength.WEAK]: 'Weak'
};
return names[type] || 'Unknown';
return names[strength] || 'Unknown';
}

// Initialize
Expand Down
Loading