react-native-keychainImplementing a Biometrics Hook in React Native
Biometrics have become a very popular way to authenticate mobile users, and going forward, even desktop users. But does this mean Biometrics implementations have to be complicated and a science?

Biometrics is a must-have for any React Native app. Users expect to be able to authenticate with your app using all the methods offered by their device. There are several great npm libraries that make integrating biometrics into your React Native app easy. However, they only provide a thin wrapper around the native biometrics SDK. It is up to you to design an API that makes working with biometrics simple and maintainable.
I recently had a chance to refactor my project’s implementation of biometrics functionality. Before, we had logic scattered across the app, often using different implementations to achieve the same effect, such as prompting for a login, or clearing the storage. I was able to come up with a reusable hook design that I’m excited to share. Today we’re going to look at writing a hook that will encapsulate all functionality related to biometrics. In the end, you’ll have an easy-to-use hook to use in your components that looks like this:
// login screen
const { isBiometricsEnabled, promptBiometrics, biometricsType } = useBiometrics()
const handleBiometricsLogin = async () => {
const { username, password } = await promptBiometrics()
login(username, password)
}
return <Button onPress={handleBiometricsLogin}>
Login with {biometricsType}
</Button>
// settings screen
const {
enableBiometrics, disableBiometrics, isBiometricsEnabled,
isBiometricsLoading, isBiometricsSupported } = useBiometrics()
const handleToggleBiometrics = () =>
isBiometricsEnabled ? disableBiometrics() : enableBiometrics(username, password)
return (
<Switch
disabled={!isBiometricsSupported}
value={isBiometricsEnabled}
onValueChange={handleToggleBiometrics}
/>
)
Prerequisites
We’re going to be using React Context to make our biometrics hook available throughout the app. To interact with the native SDKs for biometrics and storage, we’re going to be using two libraries: react-native-async-storage
We’re also going to assume you’re using Typescript, but you can still use these examples in a vanilla Javascript project - you’ll just have to remove the type definitions.
Creating the Context
We need to keep a few pieces of state around, like whether biometrics is enabled or currently loading. We’ll also need a way to access some singleton object that will contain our state (eg.
isBiometricsEnabledenableBiometricsinterface IBiometricsContext {
isBiometricsSupported: boolean
isBiometricsEnabled: boolean
isBiometricsLoading: boolean
biometricsType: Keychain.BIOMETRY_TYPE | null
enableBiometrics: (username: string; password: string) => Promise<void>
disableBiometrics: () => Promise<void>
promptBiometrics: () => Promise<false | Keychain.UserCredentials>
}
Then we can instantiate the context object. Feel free to ignore type errors here. We are going to create another default values object that will override this one when we create the provider.
const BiometricsContext = createContext<IBiometricsContext>({
isBiometricsSupported: false,
isBiometricsEnabled: false,
isBiometricsLoading: true,
biometricsType: null,
enableBiometrics: async () => undefined,
disableBiometrics: async () => undefined,
// @ts-ignore
promptBiometrics: () => {}
})
Now we have our context object, but it’s not of much use to us until we create a provider to go along with it.
Creating the Provider
We’re now going to be implementing the logic behind our Provider. All of the following code snippets belong inside the body of our
BiometricsProviderexport const BiometricsProvider: FC = ({ children }) => {
// ...
}
If this is confusing, you may find it helpful to follow along with the file containing the end result, which you can find here.
Setting up our state
Since this is a regular react component, we can store our state in a simple
useStateconst [isBiometricsSupported, setBiometricsSupported] = useState(false)
const [biometricsType, setBiometricsType] =
useState<Keychain.BIOMETRY_TYPE | null>(null)
const [isBiometricsEnabled, setBiometricsEnabled] = useState(false)
const [isBiometricsLoading, setBiometricsLoading] = useState(true)
If this is confusing, you may find it helpful to follow along with the file containing the end result, which you can find here.
Notice the initial state of
isBiometricsLoadingInitialization
We are going to use our trusty
useEffectuseEffect(() => {
const init = async () => {
// 1
const type = await Keychain.getSupportedBiometryType()
if (!type) {
setBiometricsLoading(false)
return
}
// 2
setBiometricsSupported(true)
setBiometricsType(type)
// 3
setBiometricsEnabled((await AsyncStorage.getItem('is-biometrics-enabled')) === 'true')
// 4
setBiometricsLoading(false)
}
init()
}, [])
Let’s walk through this code. First off, we are not allowed to pass an
asyncuseEffectGet the type of biometrics supported by the device. If this returns null, biometrics is unsupported and we return early.
If biometrics is supported, set
to true and store the type.isBiometricsSupportedNow we check the device storage to check if the user has previously enabled biometrics. This might seem strange, so let me explain. There exists a function to check which services we have stored credentials for:
. However, it prompts the user for biometrics authentication, because the entries are encrypted. This doesn’t make sense for our scenario - we don’t want to prompt the user to authenticate when they haven’t asked for it. Instead, when the user enables biometrics, we will simply store a value in async storage (Keychain.getAllGenericPasswordServices()) and check it during initialization. You’ll see this implementation shortly.'is-biometrics-enabled’Lastly, set
to false.isBiometricsLoading
Core Logic
Let’s take a look at implementing the core functionality of our hook: enabling and disabling biometrics, and prompting the user for a biometrics authentication. First off, let’s tackle the enable function.
const enableBiometrics = async (username: string; password: string) => {
// 1
setBiometricsLoading(true)
try {
// 2
await Keychain.setGenericPassword(username, password, {
service: 'my-app',
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
accessible: Keychain.ACCESSIBLE.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
})
// 3
await AsyncStorage.setItem('is-biometrics-enabled', 'true')
// 4
setBiometricsEnabled(true)
setBiometricsLoading(false)
} catch (error) {
// 5
setBiometricsLoading(false)
throw error
}
}
Let’s walk through this line by line:
Set
to true.isBiometricsLoadingStore the credentials, identifying our app as
. The'my-app'option controls which authentication methods can be used to access these credentials. TheaccessControloption can be used to disable credential access in certain scenarios, such as when the device is locked, or whether the credential should be shared across devices. Review the documentation foraccessibleandKeychain.ACCESS_CONTROLenums and confirm that you are using the value that matches your use case.Keychain.ACCESSIBLENext, we store our
value in'is-biometrics-enabled', so on subsequent app launches, we can know if there are credentials stored on the device without prompting the user.AsyncStorageIf all goes well, set
to true andisBiometricsEnabledto false.isBiometricsLoadingWrap it all in a try/catch block so we can set
to false in case of an error. Then, re-throw the error and let the caller deal with it 😉isBiometricsLoading
Next, let’s take a look at the disable function. This one is pretty straightforward. We simply erase the credentials from Keychain and update our
AsyncStorageenableBiometricsconst disableBiometrics = async () => {
setBiometricsLoading(true)
try {
// erase the stored password
await Keychain.resetGenericPassword({ service: 'my-app' })
// update the async storage
await AsyncStorage.setItem('is-biometrics-enabled', 'false')
// update the local state
setBiometricsEnabled(false)
setBiometricsLoading(false)
} catch (error) {
setBiometricsLoading(false)
throw error
}
}
Finally, we are ready to implement our
promptBiometricsKeychainconst promptBiometrics = async () => {
return Keychain.getGenericPassword({
service: 'my-app',
authenticationPrompt: {
title: `Sign into MyApp using ${biometricsType}`
}
})
}
Whew! All that’s left to do is assemble everything into the object that will be made accessible by our provider, and return the JSX of the Provider itself.
return (
<BiometricsContext.Provider
value={{
isBiometricsSupported,
isBiometricsEnabled,
isBiometricsLoading,
biometricsType,
enableBiometrics,
disableBiometrics,
promptBiometrics
}}
>
{children}
</BiometricsContext.Provider>
)
Let’s wrap up this with a bow and export our
useBiometricsexport default function useBiometrics() {
return useContext(BiometricsContext)
}
Don’t forget to add the
BiometricsProviderApp// App.tsx
import { BiometricsProvider } from '~/providers/biometrics-provider'
export default function App() {
<BiometricsProvider>
// whatever else was here before...
</BiometricsProvider>
}
That’s all folks! We have managed to fully encapsulate the logic and state of all biometrics functionality for our application. We can check if biometrics is enabled, prompt for a biometrics login and more, all by using the hook.
This also opens up opportunities for simplified testing - all you would need to do is write a mock hook that uses the same logic but leaves out the parts interacting with native SDKs!
We hope this post allows you to simplify your existing code, or write a new implementation with ease. Please check out the full code sample here. Happy hacking!
Newsletter Sign-up
Receive summaries directly in your inbox.




