Skip to content

Commit

Permalink
Mobile: Add support for locking the app using biometrics
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Jan 4, 2023
1 parent 5a05cc5 commit f10d9f7
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down
5 changes: 5 additions & 0 deletions packages/app-mobile/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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' }
}
Expand Down
89 changes: 89 additions & 0 deletions packages/app-mobile/components/biometrics/BiometricPopup.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 37 additions & 0 deletions packages/app-mobile/components/biometrics/sensorInfo.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
2 changes: 2 additions & 0 deletions packages/app-mobile/ios/Joplin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -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>
6 changes: 6 additions & 0 deletions packages/app-mobile/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/app-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions packages/app-mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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) {};

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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>
Expand Down
22 changes: 22 additions & 0 deletions packages/lib/models/Setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit f10d9f7

Please sign in to comment.