mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Mobile: Add support for locking the app using biometrics
This commit is contained in:
parent
5a05cc5797
commit
f10d9f75b0
@ -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
|
||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -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
|
||||
|
@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<!-- Make these features optional to enable Chromebooks -->
|
||||
<!-- https://github.com/laurent22/joplin/issues/37 -->
|
||||
|
@ -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' }
|
||||
}
|
||||
|
89
packages/app-mobile/components/biometrics/BiometricPopup.tsx
Normal file
89
packages/app-mobile/components/biometrics/BiometricPopup.tsx
Normal file
@ -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 <Button title={_('Try again')} onPress={() => setTryBiometricsCheck(true)} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ display: display ? 'flex' : 'none', position: 'absolute', zIndex: 99999, backgroundColor: '#000000', width: windowSize.width, height: windowSize.height }}>
|
||||
{renderTryAgainButton()}
|
||||
</View>
|
||||
);
|
||||
};
|
37
packages/app-mobile/components/biometrics/sensorInfo.ts
Normal file
37
packages/app-mobile/components/biometrics/sensorInfo.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import FingerprintScanner from 'react-native-fingerprint-scanner';
|
||||
|
||||
export interface SensorInfo {
|
||||
enabled: boolean;
|
||||
sensorsHaveChanged: boolean;
|
||||
supportedSensors: string;
|
||||
}
|
||||
|
||||
export default async (): Promise<SensorInfo> => {
|
||||
const enabled = Setting.value('security.biometricsEnabled');
|
||||
let hasChanged = false;
|
||||
let supportedSensors = '';
|
||||
|
||||
if (enabled) {
|
||||
try {
|
||||
const result = await FingerprintScanner.isSensorAvailable();
|
||||
supportedSensors = result;
|
||||
|
||||
if (result) {
|
||||
if (result !== Setting.value('security.biometricsSupportedSensors')) {
|
||||
hasChanged = true;
|
||||
Setting.setValue('security.biometricsSupportedSensors', result);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not check for biometrics sensor:', error);
|
||||
Setting.setValue('security.biometricsSupportedSensors', '');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
sensorsHaveChanged: hasChanged,
|
||||
supportedSensors,
|
||||
};
|
||||
};
|
@ -107,5 +107,7 @@
|
||||
<string>Light</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -234,6 +234,8 @@ PODS:
|
||||
- React-Core
|
||||
- react-native-document-picker (8.1.3):
|
||||
- React-Core
|
||||
- react-native-fingerprint-scanner (6.0.0):
|
||||
- React
|
||||
- react-native-geolocation (2.1.0):
|
||||
- React-Core
|
||||
- react-native-get-random-values (1.8.0):
|
||||
@ -367,6 +369,7 @@ DEPENDENCIES:
|
||||
- react-native-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`)
|
||||
- react-native-camera (from `../node_modules/react-native-camera`)
|
||||
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
||||
- react-native-fingerprint-scanner (from `../node_modules/react-native-fingerprint-scanner`)
|
||||
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
|
||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||
@ -452,6 +455,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native-camera"
|
||||
react-native-document-picker:
|
||||
:path: "../node_modules/react-native-document-picker"
|
||||
react-native-fingerprint-scanner:
|
||||
:path: "../node_modules/react-native-fingerprint-scanner"
|
||||
react-native-geolocation:
|
||||
:path: "../node_modules/@react-native-community/geolocation"
|
||||
react-native-get-random-values:
|
||||
@ -544,6 +549,7 @@ SPEC CHECKSUMS:
|
||||
react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52
|
||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
||||
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||
react-native-geolocation: 69f4fd37650b8e7fee91816d395e62dd16f5ab8d
|
||||
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
||||
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695
|
||||
|
@ -45,6 +45,7 @@
|
||||
"react-native-document-picker": "8.1.3",
|
||||
"react-native-dropdownalert": "4.5.1",
|
||||
"react-native-file-viewer": "2.1.5",
|
||||
"react-native-fingerprint-scanner": "6.0.0",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-get-random-values": "1.8.0",
|
||||
"react-native-image-picker": "4.10.3",
|
||||
|
@ -83,6 +83,7 @@ const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
|
||||
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
|
||||
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
|
||||
import BiometricPopup from './components/biometrics/BiometricPopup';
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetNone);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
@ -108,6 +109,7 @@ import { setRSA } from '@joplin/lib/services/e2ee/ppk';
|
||||
import RSA from './services/e2ee/RSA.react-native';
|
||||
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
|
||||
import { AppState } from './utils/types';
|
||||
import sensorInfo from './components/biometrics/sensorInfo';
|
||||
|
||||
let storeDispatch = function(_action: any) {};
|
||||
|
||||
@ -690,6 +692,7 @@ class AppComponent extends React.Component {
|
||||
this.state = {
|
||||
sideMenuContentOpacity: new Animated.Value(0),
|
||||
sideMenuWidth: this.getSideMenuWidth(),
|
||||
sensorInfo: null,
|
||||
};
|
||||
|
||||
this.lastSyncStarted_ = defaultState.syncStarted;
|
||||
@ -760,6 +763,8 @@ class AppComponent extends React.Component {
|
||||
|
||||
await initialize(this.props.dispatch);
|
||||
|
||||
this.setState({ sensorInfo: await sensorInfo() });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'APP_STATE_SET',
|
||||
state: 'ready',
|
||||
@ -931,6 +936,10 @@ class AppComponent extends React.Component {
|
||||
</View>
|
||||
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
||||
<Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '120%' }}/>
|
||||
<BiometricPopup
|
||||
themeId={this.props.themeId}
|
||||
sensorInfo={this.state.sensorInfo}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</MenuContext>
|
||||
</SideMenu>
|
||||
|
@ -1611,6 +1611,28 @@ class Setting extends BaseModel {
|
||||
storage: SettingStorage.Database,
|
||||
},
|
||||
|
||||
'security.biometricsEnabled': {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
label: () => _('Use biometrics to secure access to the app'),
|
||||
public: true,
|
||||
appTypes: [AppType.Mobile],
|
||||
},
|
||||
|
||||
'security.biometricsSupportedSensors': {
|
||||
value: '',
|
||||
type: SettingItemType.String,
|
||||
public: false,
|
||||
appTypes: [AppType.Mobile],
|
||||
},
|
||||
|
||||
'security.biometricsInitialPromptDone': {
|
||||
value: false,
|
||||
type: SettingItemType.Bool,
|
||||
public: false,
|
||||
appTypes: [AppType.Mobile],
|
||||
},
|
||||
|
||||
// 'featureFlag.syncAccurateTimestamps': {
|
||||
// value: false,
|
||||
// type: SettingItemType.Bool,
|
||||
|
10
yarn.lock
10
yarn.lock
@ -4725,6 +4725,7 @@ __metadata:
|
||||
react-native-document-picker: 8.1.3
|
||||
react-native-dropdownalert: 4.5.1
|
||||
react-native-file-viewer: 2.1.5
|
||||
react-native-fingerprint-scanner: ^6.0.0
|
||||
react-native-fs: 2.20.0
|
||||
react-native-get-random-values: 1.8.0
|
||||
react-native-image-picker: 4.10.3
|
||||
@ -27611,6 +27612,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-fingerprint-scanner@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "react-native-fingerprint-scanner@npm:6.0.0"
|
||||
peerDependencies:
|
||||
react-native: ">=0.60 <1.0.0"
|
||||
checksum: 67e1dcbf20d1a6119db4667162ff87c6ba606132c0cda790ef5c4d315e403f3253f8f4827373313562a261fca2d7cb77c8598f5f32d805ac07956b5301bce238
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-native-fs@npm:2.20.0":
|
||||
version: 2.20.0
|
||||
resolution: "react-native-fs@npm:2.20.0"
|
||||
|
Loading…
Reference in New Issue
Block a user