In our last lesson, you successfully integrated a basic biometric authentication flow. We used a straightforward plugin to create a component that could gate content behind a biometric prompt. This was a great first step into securing your application.
Today, we will build upon that foundation to gain more granular insight and control. The goal of this lesson is to not only check if biometrics are available but also to determine the specific type of hardware the device offers—be it Face ID, Touch ID, or a fingerprint sensor. This allows for a more tailored user experience, for example, by displaying a message like "Log in with Face ID" instead of a generic "Log in with biometrics."
To achieve this, we will explore a more advanced plugin that provides richer diagnostic information. This process mirrors a common real-world engineering decision: evaluating and sometimes switching libraries to meet more sophisticated requirements.
1. A More Capable Biometrics Plugin
The plugin we used previously, @capgo/capacitor-native-biometric, was excellent for its simplicity. However, for detailed hardware information, we'll turn to @aparajita/capacitor-biometric-auth. This plugin offers a comprehensive API for inspecting the biometric capabilities of a device.
Before we dive into the code, it's worth understanding the features this new plugin provides, especially its checkBiometry() method. The plugin's documentation offers a clear explanation.
This document details the plugin's API. We'll focus on the parts that describe how to check for biometric availability and what information is returned.
Please read the following sections: Start with the section on Checking availability. Pay close attention to the distinction it makes between isAvailable and strongBiometryIsAvailable, particularly on Android. Next, review the interface definition for CheckBiometryResult. This is the data structure you will be working with, so familiarize yourself with its properties like biometryType and biometryTypes. Finally, look at the BiometryType enum to see the different hardware types the plugin can identify.
As you can see from the documentation, this plugin provides a much richer dataset than a simple boolean, allowing us to build more intelligent UI and logic.
2. Switching and Re-configuring
Let's switch our project to the new plugin. This involves removing the old package and installing the new one. Since you're comfortable with bun, execute these commands from your monorepo root:
# Remove the old plugin
bun remove @capgo/capacitor-native-biometric
# Install the new plugin
bun add @aparajita/capacitor-biometric-auth
# Sync the new plugin's native code
bunx cap sync
The native configuration for this plugin is identical to the last one. We already added the NSFaceIDUsageDescription to Info.plist and the USE_BIOMETRIC permission to AndroidManifest.xml in the previous lesson, so no further changes are needed there. The bunx cap sync command ensures the new native code from @aparajita/capacitor-biometric-auth is correctly linked.
3. Implementing the Advanced Availability Check
Now, let's refactor the BiometricGuard.tsx component from the previous lesson to use the new plugin's API. Our goal is to store the entire result of the availability check and use it to display more specific information to the user.
First, let's look at the theoretical interaction flow for a similar biometric implementation that uses cryptographic keys. The general sequence of generating and using keys is a common pattern in secure systems.

While our current plugin abstracts this away, it's useful to understand the underlying cryptographic principles that make biometric authentication secure.
Now, let's update our React component. We will replace the simple isAvailable boolean with a state variable that holds the entire CheckBiometryResult object.
// src/components/BiometricGuard.tsx (Refactored)
import React, { useState, useEffect } from 'react';
// Import from the new plugin
import { BiometricAuth, BiometryType, CheckBiometryResult } from '@aparajita/capacitor-biometric-auth';
import { isPlatform } from '@ionic/react';
interface BiometricGuardProps {
children: React.ReactNode;
}
// A helper function to get a user-friendly name for the biometry type
const getBiometryName = (biometryType: BiometryType) => {
switch (biometryType) {
case BiometryType.faceId:
return 'Face ID';
case BiometryType.touchId:
return 'Touch ID';
case BiometryType.fingerprintAuthentication:
return 'Fingerprint';
case BiometryType.faceAuthentication:
return 'Face Unlock';
case BiometryType.irisAuthentication:
return 'Iris Scan';
default:
return 'Biometrics';
}
};
const BiometricGuard: React.FC<BiometricGuardProps> = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
// State to hold the detailed biometric info
const [biometricInfo, setBiometricInfo] = useState<CheckBiometryResult | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!isPlatform('capacitor')) {
console.log('Not running on a native platform. Bypassing biometrics.');
setIsAuthenticated(true);
return;
}
const checkAvailability = async () => {
try {
// Use the new checkBiometry method
const result = await BiometricAuth.checkBiometry();
setBiometricInfo(result);
} catch (e: any) {
setError('Error checking biometric availability.');
console.error(e);
}
};
checkAvailability();
}, []);
const handleAuthentication = async () => {
if (!biometricInfo?.isAvailable) {
setError('Biometric authentication is not available or not configured.');
return;
}
try {
// Use the new authenticate method
await BiometricAuth.authenticate({
reason: 'For easy and secure access',
});
// If authenticate() resolves, it was successful
setIsAuthenticated(true);
} catch (e: any) {
// The plugin throws an error on failure
setError(`Authentication failed: ${e.message}`);
console.error(e);
setIsAuthenticated(false);
}
};
if (isAuthenticated) {
return <>{children}</>;
}
// Get a user-friendly name for the button
const biometryName = biometricInfo ? getBiometryName(biometricInfo.biometryType) : 'Biometrics';
return (
<div style={{ padding: '20px' }}>
<h2>Authentication Required</h2>
<p>Please verify your identity to access this content.</p>
<button onClick={handleAuthentication} disabled={!biometricInfo?.isAvailable}>
Authenticate with {biometryName}
</button>
{biometricInfo && !biometricInfo.isAvailable && isPlatform('capacitor') && (
<p style={{ color: 'orange', marginTop: '10px' }}>
{biometricInfo.reason || 'Biometrics not configured.'}
</p>
)}
{error && <p style={{ color: 'red', marginTop: '10px' }}>{error}</p>}
</div>
);
};
export default BiometricGuard;
Key Changes in the Refactored Component:
- Imports: We now import
BiometricAuth,BiometryType, andCheckBiometryResultfrom@aparajita/capacitor-biometric-auth. - State: The
isBiometricAvailableboolean is replaced withbiometricInfo, which can hold the fullCheckBiometryResultobject. - Availability Check:
useEffectnow callsBiometricAuth.checkBiometry()and stores the entire result. - Authentication Call:
BiometricAuth.authenticate()is used. A key difference from the previous plugin is that it resolves on success and rejects on failure, which aligns well with standardasync/awaiterror handling usingtry...catch. - Dynamic UI: A helper function,
getBiometryName, uses thebiometryTypefrom our state to create a specific label for the authentication button (e.g., "Authenticate with Face ID"). We also display thereasonfor unavailability if the check fails.
4. Handling Platform Differences and App State
As you discovered in the documentation, Android's biometric landscape is more fragmented than iOS's.
- iOS: Life is simple. All supported biometrics (Touch ID, Face ID) are considered "strong." The
isAvailableandstrongBiometryIsAvailableflags inCheckBiometryResultwill always be identical. - Android: There's a distinction between "strong" and "weak" biometrics. A device might have face unlock that is considered "weak" and only suitable for unlocking the screen, not for authenticating within apps. The
checkBiometry()result reflects this:isAvailablewill betrueif any enrolled biometry can be used by the app (weak or strong).strongBiometryIsAvailableis onlytrueif an enrolled "strong" biometric (usually fingerprint) is available.- The documentation wisely advises relying on these booleans rather than the
biometryTypesarray on Android, as the hardware might be reported even if it's not usable by apps.
For most security purposes, you'll want to check isAvailable, but for high-security features, you might eventually decide to check strongBiometryIsAvailable.
Responding to Changes
What if a user enables or disables Face ID in their device settings while your app is in the background? Your biometricInfo state would become stale. The plugin provides a listener for the app's resume event to handle this gracefully.
This section explains how to listen for the app resuming from the background.
Read the short section on Handling app resume to understand the purpose of the addResumeListener method.
You can integrate this into your useEffect hook to ensure your component's state is always fresh.
// Inside BiometricGuard.tsx, within the useEffect hook
useEffect(() => {
// ... (existing code)
const checkAvailability = async () => {
// ... (existing check)
};
checkAvailability();
// Add the resume listener
const listener = BiometricAuth.addResumeListener(async (result) => {
console.log('App resumed, re-checking biometrics...');
setBiometricInfo(result);
});
// Cleanup function to remove the listener when the component unmounts
return () => {
listener.then(l => l.remove());
};
}, []);
This addition makes your implementation robust by automatically re-validating biometric status whenever the user returns to the app.
Conclusion
In this lesson, you elevated your biometric implementation from a simple check to a detailed diagnostic. This gives you the power to create a more polished and informative user experience.
Here are the main takeaways:
- Choosing the right plugin involves balancing simplicity with the need for detailed features.
- The
@aparajita/capacitor-biometric-authplugin provides acheckBiometry()method that returns a richCheckBiometryResultobject. - You can use the
biometryTypeproperty to identify the specific hardware (Face ID, Touch ID, etc.) and tailor your UI accordingly. - Understanding the platform-specific nuances, like the strong vs. weak biometry distinction on Android, is crucial for building a reliable app.
- Handling app state changes, such as resuming from the background, by using
addResumeListenerensures your app's security status remains current.
In the next lesson, we will encapsulate all this logic into a reusable React hook (useBiometricAuth). This will abstract away the complexity of state management, availability checks, and authentication calls, allowing you to secure any component or route in your app with a single, clean line of code.
Can't find a good explanation? Sign up and we'll make it for you
Sign up