diff --git a/.eslintignore b/.eslintignore
index e64e5ecea..a04dd0f0f 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -990,6 +990,12 @@ packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map
+packages/app-mobile/components/biometrics/BiometricPopup.d.ts
+packages/app-mobile/components/biometrics/BiometricPopup.js
+packages/app-mobile/components/biometrics/BiometricPopup.js.map
+packages/app-mobile/components/biometrics/sensorInfo.d.ts
+packages/app-mobile/components/biometrics/sensorInfo.js
+packages/app-mobile/components/biometrics/sensorInfo.js.map
packages/app-mobile/components/getResponsiveValue.d.ts
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/getResponsiveValue.js.map
diff --git a/.gitignore b/.gitignore
index 1d1b9de1a..f71d922ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -978,6 +978,12 @@ packages/app-mobile/components/SelectDateTimeDialog.js.map
packages/app-mobile/components/SideMenu.d.ts
packages/app-mobile/components/SideMenu.js
packages/app-mobile/components/SideMenu.js.map
+packages/app-mobile/components/biometrics/BiometricPopup.d.ts
+packages/app-mobile/components/biometrics/BiometricPopup.js
+packages/app-mobile/components/biometrics/BiometricPopup.js.map
+packages/app-mobile/components/biometrics/sensorInfo.d.ts
+packages/app-mobile/components/biometrics/sensorInfo.js
+packages/app-mobile/components/biometrics/sensorInfo.js.map
packages/app-mobile/components/getResponsiveValue.d.ts
packages/app-mobile/components/getResponsiveValue.js
packages/app-mobile/components/getResponsiveValue.js.map
diff --git a/packages/app-mobile/android/app/src/main/AndroidManifest.xml b/packages/app-mobile/android/app/src/main/AndroidManifest.xml
index c590d1a7b..dbdcd8303 100644
--- a/packages/app-mobile/android/app/src/main/AndroidManifest.xml
+++ b/packages/app-mobile/android/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
diff --git a/packages/app-mobile/android/build.gradle b/packages/app-mobile/android/build.gradle
index 93b640201..c90394821 100644
--- a/packages/app-mobile/android/build.gradle
+++ b/packages/app-mobile/android/build.gradle
@@ -46,6 +46,11 @@ allprojects {
excludeGroup "com.facebook.react"
}
}
+ maven {
+ // Required by react-native-fingerprint-scanner
+ // https://github.com/hieuvp/react-native-fingerprint-scanner/issues/192
+ url "https://maven.aliyun.com/repository/jcenter"
+ }
google()
maven { url 'https://www.jitpack.io' }
}
diff --git a/packages/app-mobile/components/biometrics/BiometricPopup.tsx b/packages/app-mobile/components/biometrics/BiometricPopup.tsx
new file mode 100644
index 000000000..db501bdfb
--- /dev/null
+++ b/packages/app-mobile/components/biometrics/BiometricPopup.tsx
@@ -0,0 +1,89 @@
+const React = require('react');
+import Setting from '@joplin/lib/models/Setting';
+import { useEffect, useMemo, useState } from 'react';
+import { View, Dimensions, Alert, Button } from 'react-native';
+import FingerprintScanner from 'react-native-fingerprint-scanner';
+import { SensorInfo } from './sensorInfo';
+import { _ } from '@joplin/lib/locale';
+
+interface Props {
+ themeId: number;
+ sensorInfo: SensorInfo;
+}
+
+export default (props: Props) => {
+ const [initialPromptDone, setInitialPromptDone] = useState(false); // Setting.value('security.biometricsInitialPromptDone'));
+ const [display, setDisplay] = useState(!!props.sensorInfo.supportedSensors && (props.sensorInfo.enabled || !initialPromptDone));
+ const [tryBiometricsCheck, setTryBiometricsCheck] = useState(initialPromptDone);
+
+ useEffect(() => {
+ if (!display || !tryBiometricsCheck) return;
+
+ const biometricsCheck = async () => {
+ try {
+ await FingerprintScanner.authenticate({ description: _('Verify your identity') });
+ setTryBiometricsCheck(false);
+ setDisplay(false);
+ } catch (error) {
+ Alert.alert(_('Could not verify your identify'), error.message);
+ setTryBiometricsCheck(false);
+ } finally {
+ FingerprintScanner.release();
+ }
+ };
+
+ void biometricsCheck();
+ }, [display, tryBiometricsCheck]);
+
+ useEffect(() => {
+ if (initialPromptDone) return;
+ if (!display) return;
+
+ const complete = (enableBiometrics: boolean) => {
+ setInitialPromptDone(true);
+ Setting.setValue('security.biometricsInitialPromptDone', true);
+ Setting.setValue('security.biometricsEnabled', enableBiometrics);
+ if (!enableBiometrics) {
+ setDisplay(false);
+ setTryBiometricsCheck(false);
+ } else {
+ setTryBiometricsCheck(true);
+ }
+ };
+
+ Alert.alert(
+ _('Enable biometrics authentication?'),
+ _('Use your biometrics to secure access to your application. You can always set it up later in Settings.'),
+ [
+ {
+ text: _('Enable'),
+ onPress: () => complete(true),
+ style: 'default',
+ },
+ {
+ text: _('Not now'),
+ onPress: () => complete(false),
+ style: 'cancel',
+ },
+ ]
+ );
+ }, [initialPromptDone, props.sensorInfo.supportedSensors, display]);
+
+ const windowSize = useMemo(() => {
+ return {
+ width: Dimensions.get('window').width,
+ height: Dimensions.get('window').height,
+ };
+ }, []);
+
+ const renderTryAgainButton = () => {
+ if (!display || tryBiometricsCheck || !initialPromptDone) return null;
+ return