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.d.ts
|
||||||
packages/app-mobile/components/SideMenu.js
|
packages/app-mobile/components/SideMenu.js
|
||||||
packages/app-mobile/components/SideMenu.js.map
|
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.d.ts
|
||||||
packages/app-mobile/components/getResponsiveValue.js
|
packages/app-mobile/components/getResponsiveValue.js
|
||||||
packages/app-mobile/components/getResponsiveValue.js.map
|
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.d.ts
|
||||||
packages/app-mobile/components/SideMenu.js
|
packages/app-mobile/components/SideMenu.js
|
||||||
packages/app-mobile/components/SideMenu.js.map
|
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.d.ts
|
||||||
packages/app-mobile/components/getResponsiveValue.js
|
packages/app-mobile/components/getResponsiveValue.js
|
||||||
packages/app-mobile/components/getResponsiveValue.js.map
|
packages/app-mobile/components/getResponsiveValue.js.map
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
|
||||||
<!-- Make these features optional to enable Chromebooks -->
|
<!-- Make these features optional to enable Chromebooks -->
|
||||||
<!-- https://github.com/laurent22/joplin/issues/37 -->
|
<!-- https://github.com/laurent22/joplin/issues/37 -->
|
||||||
|
@ -46,6 +46,11 @@ allprojects {
|
|||||||
excludeGroup "com.facebook.react"
|
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()
|
google()
|
||||||
maven { url 'https://www.jitpack.io' }
|
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>
|
<string>Light</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
<string>$(PRODUCT_NAME) requires FaceID access to secure access to the application</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -234,6 +234,8 @@ PODS:
|
|||||||
- React-Core
|
- React-Core
|
||||||
- react-native-document-picker (8.1.3):
|
- react-native-document-picker (8.1.3):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- react-native-fingerprint-scanner (6.0.0):
|
||||||
|
- React
|
||||||
- react-native-geolocation (2.1.0):
|
- react-native-geolocation (2.1.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-get-random-values (1.8.0):
|
- 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-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`)
|
||||||
- react-native-camera (from `../node_modules/react-native-camera`)
|
- react-native-camera (from `../node_modules/react-native-camera`)
|
||||||
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
- 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-geolocation (from `../node_modules/@react-native-community/geolocation`)"
|
||||||
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
|
||||||
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
|
||||||
@ -452,6 +455,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native-camera"
|
:path: "../node_modules/react-native-camera"
|
||||||
react-native-document-picker:
|
react-native-document-picker:
|
||||||
:path: "../node_modules/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:
|
react-native-geolocation:
|
||||||
:path: "../node_modules/@react-native-community/geolocation"
|
:path: "../node_modules/@react-native-community/geolocation"
|
||||||
react-native-get-random-values:
|
react-native-get-random-values:
|
||||||
@ -544,6 +549,7 @@ SPEC CHECKSUMS:
|
|||||||
react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52
|
react-native-alarm-notification: 4e150e89c1707e057bc5e8c87ab005f1ea4b8d52
|
||||||
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f
|
||||||
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
||||||
|
react-native-fingerprint-scanner: ac6656f18c8e45a7459302b84da41a44ad96dbbe
|
||||||
react-native-geolocation: 69f4fd37650b8e7fee91816d395e62dd16f5ab8d
|
react-native-geolocation: 69f4fd37650b8e7fee91816d395e62dd16f5ab8d
|
||||||
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a
|
||||||
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695
|
react-native-image-picker: 60f4246eb5bb7187fc15638a8c1f13abd3820695
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
"react-native-document-picker": "8.1.3",
|
"react-native-document-picker": "8.1.3",
|
||||||
"react-native-dropdownalert": "4.5.1",
|
"react-native-dropdownalert": "4.5.1",
|
||||||
"react-native-file-viewer": "2.1.5",
|
"react-native-file-viewer": "2.1.5",
|
||||||
|
"react-native-fingerprint-scanner": "6.0.0",
|
||||||
"react-native-fs": "2.20.0",
|
"react-native-fs": "2.20.0",
|
||||||
"react-native-get-random-values": "1.8.0",
|
"react-native-get-random-values": "1.8.0",
|
||||||
"react-native-image-picker": "4.10.3",
|
"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 SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js');
|
||||||
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
|
const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js');
|
||||||
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
|
const SyncTargetAmazonS3 = require('@joplin/lib/SyncTargetAmazonS3.js');
|
||||||
|
import BiometricPopup from './components/biometrics/BiometricPopup';
|
||||||
|
|
||||||
SyncTargetRegistry.addClass(SyncTargetNone);
|
SyncTargetRegistry.addClass(SyncTargetNone);
|
||||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||||
@ -108,6 +109,7 @@ import { setRSA } from '@joplin/lib/services/e2ee/ppk';
|
|||||||
import RSA from './services/e2ee/RSA.react-native';
|
import RSA from './services/e2ee/RSA.react-native';
|
||||||
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
|
import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
|
||||||
import { AppState } from './utils/types';
|
import { AppState } from './utils/types';
|
||||||
|
import sensorInfo from './components/biometrics/sensorInfo';
|
||||||
|
|
||||||
let storeDispatch = function(_action: any) {};
|
let storeDispatch = function(_action: any) {};
|
||||||
|
|
||||||
@ -690,6 +692,7 @@ class AppComponent extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
sideMenuContentOpacity: new Animated.Value(0),
|
sideMenuContentOpacity: new Animated.Value(0),
|
||||||
sideMenuWidth: this.getSideMenuWidth(),
|
sideMenuWidth: this.getSideMenuWidth(),
|
||||||
|
sensorInfo: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.lastSyncStarted_ = defaultState.syncStarted;
|
this.lastSyncStarted_ = defaultState.syncStarted;
|
||||||
@ -760,6 +763,8 @@ class AppComponent extends React.Component {
|
|||||||
|
|
||||||
await initialize(this.props.dispatch);
|
await initialize(this.props.dispatch);
|
||||||
|
|
||||||
|
this.setState({ sensorInfo: await sensorInfo() });
|
||||||
|
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: 'APP_STATE_SET',
|
type: 'APP_STATE_SET',
|
||||||
state: 'ready',
|
state: 'ready',
|
||||||
@ -931,6 +936,10 @@ class AppComponent extends React.Component {
|
|||||||
</View>
|
</View>
|
||||||
<DropdownAlert ref={(ref: any) => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
|
<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%' }}/>
|
<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>
|
</SafeAreaView>
|
||||||
</MenuContext>
|
</MenuContext>
|
||||||
</SideMenu>
|
</SideMenu>
|
||||||
|
@ -1611,6 +1611,28 @@ class Setting extends BaseModel {
|
|||||||
storage: SettingStorage.Database,
|
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': {
|
// 'featureFlag.syncAccurateTimestamps': {
|
||||||
// value: false,
|
// value: false,
|
||||||
// type: SettingItemType.Bool,
|
// type: SettingItemType.Bool,
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -4725,6 +4725,7 @@ __metadata:
|
|||||||
react-native-document-picker: 8.1.3
|
react-native-document-picker: 8.1.3
|
||||||
react-native-dropdownalert: 4.5.1
|
react-native-dropdownalert: 4.5.1
|
||||||
react-native-file-viewer: 2.1.5
|
react-native-file-viewer: 2.1.5
|
||||||
|
react-native-fingerprint-scanner: ^6.0.0
|
||||||
react-native-fs: 2.20.0
|
react-native-fs: 2.20.0
|
||||||
react-native-get-random-values: 1.8.0
|
react-native-get-random-values: 1.8.0
|
||||||
react-native-image-picker: 4.10.3
|
react-native-image-picker: 4.10.3
|
||||||
@ -27611,6 +27612,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-native-fs@npm:2.20.0":
|
||||||
version: 2.20.0
|
version: 2.20.0
|
||||||
resolution: "react-native-fs@npm:2.20.0"
|
resolution: "react-native-fs@npm:2.20.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user