Skip to content

Commit 108b30d

Browse files
authored
Merge pull request #63 from Cap-go/fix_isavailable_authentication_strength
feat: isAvailable return stength instead of specific factors
2 parents 4735379 + 6b227e4 commit 108b30d

7 files changed

Lines changed: 248 additions & 107 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,4 @@ captures
6868

6969
# External native build folder generated in Android Studio 2.2 and later
7070
.externalNativeBuild
71+
*/.codex/*

README.md

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,13 @@ Get the native Capacitor plugin version.
236236

237237
#### AvailableResult
238238

239-
| Prop | Type |
240-
| ------------------ | ----------------------------------------------------- |
241-
| **`isAvailable`** | <code>boolean</code> |
242-
| **`biometryType`** | <code><a href="#biometrytype">BiometryType</a></code> |
243-
| **`errorCode`** | <code>number</code> |
239+
Result from isAvailable() method indicating biometric authentication availability.
240+
241+
| Prop | Type | Description |
242+
| ---------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
243+
| **`isAvailable`** | <code>boolean</code> | Whether authentication is available (biometric or fallback if useFallback is true) |
244+
| **`authenticationStrength`** | <code><a href="#authenticationstrength">AuthenticationStrength</a></code> | The strength of available authentication method (STRONG, WEAK, or NONE) |
245+
| **`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. |
244246

245247

246248
#### IsAvailableOptions
@@ -313,6 +315,34 @@ Get the native Capacitor plugin version.
313315
### Enums
314316

315317

318+
#### AuthenticationStrength
319+
320+
| Members | Value | Description |
321+
| ------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
322+
| **`NONE`** | <code>0</code> | No authentication available, even if PIN is available but useFallback = false |
323+
| **`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. |
324+
| **`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). |
325+
326+
327+
#### BiometricAuthError
328+
329+
| Members | Value | Description |
330+
| ----------------------------- | --------------- | ------------------------------------------------------------------------------------- |
331+
| **`UNKNOWN_ERROR`** | <code>0</code> | Unknown error occurred |
332+
| **`BIOMETRICS_UNAVAILABLE`** | <code>1</code> | Biometrics are unavailable (no hardware or hardware error) Platform: Android, iOS |
333+
| **`USER_LOCKOUT`** | <code>2</code> | User has been locked out due to too many failed attempts Platform: Android, iOS |
334+
| **`BIOMETRICS_NOT_ENROLLED`** | <code>3</code> | No biometrics are enrolled on the device Platform: Android, iOS |
335+
| **`USER_TEMPORARY_LOCKOUT`** | <code>4</code> | User is temporarily locked out (Android: 30 second lockout) Platform: Android |
336+
| **`AUTHENTICATION_FAILED`** | <code>10</code> | Authentication failed (user did not authenticate successfully) Platform: Android, iOS |
337+
| **`APP_CANCEL`** | <code>11</code> | App canceled the authentication (iOS only) Platform: iOS |
338+
| **`INVALID_CONTEXT`** | <code>12</code> | Invalid context (iOS only) Platform: iOS |
339+
| **`NOT_INTERACTIVE`** | <code>13</code> | Authentication was not interactive (iOS only) Platform: iOS |
340+
| **`PASSCODE_NOT_SET`** | <code>14</code> | Passcode/PIN is not set on the device Platform: Android, iOS |
341+
| **`SYSTEM_CANCEL`** | <code>15</code> | System canceled the authentication (e.g., due to screen lock) Platform: Android, iOS |
342+
| **`USER_CANCEL`** | <code>16</code> | User canceled the authentication Platform: Android, iOS |
343+
| **`USER_FALLBACK`** | <code>17</code> | User chose to use fallback authentication method Platform: Android, iOS |
344+
345+
316346
#### BiometryType
317347

318348
| Members | Value |

android/src/main/java/ee/forgr/biometric/NativeBiometric.java

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public class NativeBiometric extends Plugin {
6161
private static final int IRIS_AUTHENTICATION = 5;
6262
private static final int MULTIPLE = 6;
6363

64+
// AuthenticationStrength enum values
65+
private static final int AUTH_STRENGTH_NONE = 0;
66+
private static final int AUTH_STRENGTH_STRONG = 1;
67+
private static final int AUTH_STRENGTH_WEAK = 2;
68+
6469
private KeyStore keyStore;
6570
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
6671
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
@@ -72,82 +77,71 @@ public class NativeBiometric extends Plugin {
7277

7378
private SharedPreferences encryptedSharedPreferences;
7479

75-
private int getAvailableFeature() {
76-
// default to none
77-
BiometricManager biometricManager = BiometricManager.from(getContext());
78-
79-
// Check for biometric capabilities
80-
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
81-
int canAuthenticate = biometricManager.canAuthenticate(authenticators);
82-
83-
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
84-
// Check specific features
85-
PackageManager pm = getContext().getPackageManager();
86-
boolean hasFinger = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
87-
boolean hasIris = false;
88-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
89-
hasIris = pm.hasSystemFeature(PackageManager.FEATURE_IRIS);
90-
}
91-
92-
// For face, we rely on BiometricManager since it's more reliable
93-
boolean hasFace = false;
94-
try {
95-
// Try to create a face authentication prompt - if it succeeds, face auth is available
96-
androidx.biometric.BiometricPrompt.PromptInfo promptInfo = new androidx.biometric.BiometricPrompt.PromptInfo.Builder()
97-
.setTitle("Test")
98-
.setNegativeButtonText("Cancel")
99-
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
100-
.build();
101-
hasFace = true;
102-
} catch (Exception e) {
103-
System.out.println("Error creating face authentication prompt: " + e.getMessage());
104-
}
105-
106-
// Determine the type based on available features
107-
if (hasFinger && (hasFace || hasIris)) {
108-
return MULTIPLE;
109-
} else if (hasFinger) {
110-
return FINGERPRINT;
111-
} else if (hasFace) {
112-
return FACE_AUTHENTICATION;
113-
} else if (hasIris) {
114-
return IRIS_AUTHENTICATION;
115-
}
116-
}
117-
118-
return NONE;
119-
}
120-
12180
@PluginMethod
12281
public void isAvailable(PluginCall call) {
12382
JSObject ret = new JSObject();
12483

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

12786
BiometricManager biometricManager = BiometricManager.from(getContext());
128-
int authenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
129-
if (useFallback) {
130-
authenticators |= BiometricManager.Authenticators.DEVICE_CREDENTIAL;
131-
}
132-
int canAuthenticateResult = biometricManager.canAuthenticate(authenticators);
133-
// Using deviceHasCredentials instead of canAuthenticate(DEVICE_CREDENTIAL)
134-
// > "Developers that wish to check for the presence of a PIN, pattern, or password on these versions should instead use isDeviceSecure."
135-
// @see https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate(int)
87+
88+
// Check for strong biometrics first
89+
int strongAuthenticators = BiometricManager.Authenticators.BIOMETRIC_STRONG;
90+
int strongResult = biometricManager.canAuthenticate(strongAuthenticators);
91+
boolean hasStrongBiometric = (strongResult == BiometricManager.BIOMETRIC_SUCCESS);
92+
93+
// Check for weak biometrics
94+
int weakAuthenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK;
95+
int weakResult = biometricManager.canAuthenticate(weakAuthenticators);
96+
boolean hasWeakBiometric = (weakResult == BiometricManager.BIOMETRIC_SUCCESS);
97+
98+
// Check if device has credentials (PIN/pattern/password)
13699
boolean fallbackAvailable = useFallback && this.deviceHasCredentials();
137-
if (useFallback && !fallbackAvailable) {
138-
canAuthenticateResult = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
139-
}
140100

141-
boolean isAvailable = (canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS || fallbackAvailable);
142-
ret.put("isAvailable", isAvailable);
101+
// Determine authentication strength
102+
int authenticationStrength = AUTH_STRENGTH_NONE;
103+
boolean isAvailable = false;
104+
105+
if (hasStrongBiometric) {
106+
// Strong biometric available (fingerprints on devices that consider them strong)
107+
authenticationStrength = AUTH_STRENGTH_STRONG;
108+
isAvailable = true;
109+
} else if (hasWeakBiometric) {
110+
// Only weak biometric available (face on devices that consider it weak)
111+
authenticationStrength = AUTH_STRENGTH_WEAK;
112+
isAvailable = true;
113+
} else if (fallbackAvailable) {
114+
// No biometrics but fallback (PIN/pattern/password) is available
115+
// PIN/pattern/password is ALWAYS considered WEAK, never STRONG
116+
authenticationStrength = AUTH_STRENGTH_WEAK;
117+
isAvailable = true;
118+
}
143119

120+
// Handle error codes when authentication is not available
144121
if (!isAvailable) {
145-
// BiometricManager Error Constants use the same values as BiometricPrompt's Constants. So we can reuse our
146-
int pluginErrorCode = AuthActivity.convertToPluginErrorCode(canAuthenticateResult);
122+
int biometricManagerErrorCode;
123+
124+
// Prefer the error from strong biometric check if it failed
125+
if (strongResult != BiometricManager.BIOMETRIC_SUCCESS) {
126+
biometricManagerErrorCode = strongResult;
127+
} else if (weakResult != BiometricManager.BIOMETRIC_SUCCESS) {
128+
// Otherwise use error from weak biometric check if it failed
129+
biometricManagerErrorCode = weakResult;
130+
} else {
131+
// No biometrics available at all
132+
// BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE indicates that biometric hardware is unavailable
133+
// or cannot be accessed. This constant value may vary across Android versions, so we explicitly
134+
// use the constant rather than assuming its numeric value.
135+
biometricManagerErrorCode = BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE;
136+
}
137+
138+
// Convert BiometricManager error codes to plugin error codes
139+
int pluginErrorCode = convertBiometricManagerErrorToPluginError(biometricManagerErrorCode);
147140
ret.put("errorCode", pluginErrorCode);
148141
}
149142

150-
ret.put("biometryType", getAvailableFeature());
143+
ret.put("isAvailable", isAvailable);
144+
ret.put("authenticationStrength", authenticationStrength);
151145
call.resolve(ret);
152146
}
153147

@@ -463,6 +457,25 @@ private boolean deviceHasCredentials() {
463457
return keyguardManager.isDeviceSecure();
464458
}
465459

460+
/**
461+
* Convert BiometricManager error codes to plugin error codes
462+
* BiometricManager constants have different values than BiometricPrompt constants
463+
*/
464+
private int convertBiometricManagerErrorToPluginError(int errorCode) {
465+
switch (errorCode) {
466+
case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
467+
case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
468+
case BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED:
469+
return 1; // BIOMETRICS_UNAVAILABLE
470+
case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
471+
return 3; // BIOMETRICS_NOT_ENROLLED
472+
case BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED:
473+
return 1; // BIOMETRICS_UNAVAILABLE (security update required, treat as unavailable)
474+
default:
475+
return 0; // UNKNOWN_ERROR
476+
}
477+
}
478+
466479
@PluginMethod
467480
public void getPluginVersion(final PluginCall call) {
468481
try {

example-app/simple-test.html

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ <h2>Console Output</h2>
9494
</div>
9595

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

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

225-
function getBiometryName(type) {
225+
function getAuthenticationStrengthName(strength) {
226226
const names = {
227-
0: 'None',
228-
1: 'Touch ID',
229-
2: 'Face ID',
230-
3: 'Fingerprint',
231-
4: 'Face Authentication',
232-
5: 'Iris Authentication',
233-
6: 'Multiple'
227+
[AuthenticationStrength.NONE]: 'None',
228+
[AuthenticationStrength.STRONG]: 'Strong',
229+
[AuthenticationStrength.WEAK]: 'Weak'
234230
};
235-
return names[type] || 'Unknown';
231+
return names[strength] || 'Unknown';
236232
}
237233

238234
// Hide splash screen

example-app/src/js/biometric-tester.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
1+
import { NativeBiometric, AuthenticationStrength } from '@capgo/capacitor-native-biometric';
22
import { SplashScreen } from '@capacitor/splash-screen';
33

44
// Simple function-based approach - no Web Components
@@ -110,6 +110,12 @@ function createBiometricTester() {
110110
<div class="section">
111111
<h2>Biometric Status</h2>
112112
<div id="biometric-status" class="status">Checking...</div>
113+
<div class="form-group" style="margin-bottom: 10px;">
114+
<label style="display: flex; align-items: center; cursor: pointer;">
115+
<input type="checkbox" id="use-fallback-check" style="width: auto; margin-right: 8px;">
116+
Use Fallback (passcode/PIN)
117+
</label>
118+
</div>
113119
<button class="button" id="check-availability">Check Availability</button>
114120
<div id="biometric-info" class="info" style="display: none;"></div>
115121
</div>
@@ -167,6 +173,7 @@ function createBiometricTester() {
167173
// Add event listeners
168174
const elements = {
169175
checkAvailability: container.querySelector('#check-availability'),
176+
useFallbackCheck: container.querySelector('#use-fallback-check'),
170177
verifyIdentity: container.querySelector('#verify-identity'),
171178
verifyIdentityFallback: container.querySelector('#verify-identity-fallback'),
172179
setCredentials: container.querySelector('#set-credentials'),
@@ -227,15 +234,17 @@ function createBiometricTester() {
227234
// Functions
228235
async function checkAvailability() {
229236
try {
230-
addConsoleLog('INFO', ['Checking biometric availability...']);
231-
const result = await NativeBiometric.isAvailable();
237+
const useFallback = elements.useFallbackCheck.checked;
238+
addConsoleLog('INFO', [`Checking biometric availability (useFallback: ${useFallback})...`]);
239+
const result = await NativeBiometric.isAvailable({ useFallback });
232240
addConsoleLog('INFO', ['Availability result:', result]);
233241

234242
if (result.isAvailable) {
235-
elements.biometricStatus.textContent = `✅ Biometrics Available (${getBiometryTypeName(result.biometryType)})`;
243+
const strengthName = getAuthenticationStrengthName(result.authenticationStrength);
244+
elements.biometricStatus.textContent = `✅ Biometrics Available (${strengthName})`;
236245
elements.biometricStatus.className = 'status available';
237246
elements.biometricInfo.style.display = 'block';
238-
elements.biometricInfo.innerHTML = `<strong>Biometry Type:</strong> ${getBiometryTypeName(result.biometryType)} (${result.biometryType})`;
247+
elements.biometricInfo.innerHTML = `<strong>Authentication Strength:</strong> ${strengthName} (${result.authenticationStrength})`;
239248
} else {
240249
elements.biometricStatus.textContent = `❌ Biometrics Not Available (Error: ${result.errorCode || 'Unknown'})`;
241250
elements.biometricStatus.className = 'status unavailable';
@@ -367,17 +376,13 @@ function createBiometricTester() {
367376
}
368377
}
369378

370-
function getBiometryTypeName(type) {
379+
function getAuthenticationStrengthName(strength) {
371380
const names = {
372-
0: 'None',
373-
1: 'Touch ID',
374-
2: 'Face ID',
375-
3: 'Fingerprint',
376-
4: 'Face Authentication',
377-
5: 'Iris Authentication',
378-
6: 'Multiple'
381+
[AuthenticationStrength.NONE]: 'None',
382+
[AuthenticationStrength.STRONG]: 'Strong',
383+
[AuthenticationStrength.WEAK]: 'Weak'
379384
};
380-
return names[type] || 'Unknown';
385+
return names[strength] || 'Unknown';
381386
}
382387

383388
// Initialize

0 commit comments

Comments
 (0)