Skip to content

Commit

Permalink
chore: [IA-217] Track biometric capabilities (pagopa#3393)
Browse files Browse the repository at this point in the history
* Refactor biometric module and add tests

* Track biometrics type

* Fix lint error

* Improve type safety
  • Loading branch information
pietro909 authored Sep 22, 2021
1 parent b9c04e0 commit 22a1e49
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 109 deletions.
5 changes: 5 additions & 0 deletions ts/__mocks__/react-native-fingerprint-scanner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
isSensorAvailable: () => Promise.resolve("Touch ID"),
authenticate: () => Promise.resolve(),
release: () => Promise.resolve()
};
8 changes: 4 additions & 4 deletions ts/components/Pinpad/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { Text, View } from "native-base";
import * as React from "react";
import { Alert, Dimensions, StyleSheet, ViewStyle } from "react-native";
import I18n from "../../i18n";
import { BiometryPrintableSimpleType } from "../../screens/onboarding/FingerprintScreen";
import customVariables from "../../theme/variables";
import { PinString } from "../../types/PinString";
import { ComponentProps } from "../../types/react";
import { PIN_LENGTH, PIN_LENGTH_SIX } from "../../utils/constants";
import { ShakeAnimation } from "../animations/ShakeAnimation";
import { Link } from "../core/typography/Link";
import { BiometricsValidType } from "../../utils/biometrics";
import InputPlaceHolder from "./InputPlaceholder";
import { DigitRpr, KeyPad } from "./KeyPad";

Expand Down Expand Up @@ -77,7 +77,7 @@ class Pinpad extends React.PureComponent<Props, State> {
* the available biometry functionality available on the device
*/
private getBiometryIconName(
biometryPrintableSimpleType: BiometryPrintableSimpleType
biometryPrintableSimpleType: BiometricsValidType
): DigitRpr {
switch (biometryPrintableSimpleType) {
case "BIOMETRICS":
Expand Down Expand Up @@ -113,10 +113,10 @@ class Pinpad extends React.PureComponent<Props, State> {
};

/**
* the pad can be componed by
* The pad can be composed by
* - strings
* - chars
* - icons (from icon font): they has to be declared as 'icon:<ICONAME>' (width 48) or 'sicon:<ICONAME>' (width 17)
* - icons (from icon font): they has to be declared as 'icon:<ICON_NAME>' (width 48) or 'sicon:<ICON_NAME>' (width 17)
*/
private pinPadDigits = (): ComponentProps<typeof KeyPad>["digits"] => {
const { pinPadValues } = this.state;
Expand Down
5 changes: 4 additions & 1 deletion ts/mixpanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isScreenReaderEnabled } from "./utils/accessibility";
import { getAppVersion } from "./utils/appVersion";
import { isAndroid, isIos } from "./utils/platform";
import { getDeviceId, getFontScale } from "./utils/device";
import { getBiometricsType } from "./utils/biometrics";

// eslint-disable-next-line
export let mixpanel: MixpanelInstance | undefined;
Expand Down Expand Up @@ -32,11 +33,13 @@ const setupMixpanel = async (mp: MixpanelInstance) => {
await mp.disableIpAddressGeolocalization();
}
const fontScale = await getFontScale();
const biometricTechnology = await getBiometricsType();
await mp.registerSuperProperties({
isScreenReaderEnabled: screenReaderEnabled,
fontScale,
appReadableVersion: getAppVersion(),
colorScheme: Appearance.getColorScheme()
colorScheme: Appearance.getColorScheme(),
biometricTechnology
});
// Identify the user using the device uniqueId
await mp.identify(getDeviceId());
Expand Down
20 changes: 9 additions & 11 deletions ts/sagas/startup/checkAcknowledgedFingerprintSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import { navigateToOnboardingFingerprintScreenAction } from "../../store/actions
import { fingerprintAcknowledge } from "../../store/actions/onboarding";
import { preferenceFingerprintIsEnabledSaveSuccess } from "../../store/actions/persistedPreferences";
import { isFingerprintAcknowledgedSelector } from "../../store/reducers/onboarding";
import { getFingerprintSettings } from "../../utils/biometric";

export type BiometrySimpleType =
| "BIOMETRICS"
| "FACE_ID"
| "TOUCH_ID"
| "UNAVAILABLE";
import {
BiometricsType,
getBiometricsType,
isBiometricsValidType
} from "../../utils/biometrics";

/**
* Query TouchID library to retrieve availability information. The ONLY cases
Expand All @@ -23,19 +21,19 @@ export type BiometrySimpleType =
function* onboardFingerprintIfAvailableSaga(): Generator<
Effect,
void,
BiometrySimpleType
BiometricsType
> {
// Check if user device has biometric recognition feature by trying to
// query data from TouchID library
const biometryTypeOrUnsupportedReason = yield call(getFingerprintSettings);
const biometricsType = yield call(getBiometricsType);

if (biometryTypeOrUnsupportedReason !== "UNAVAILABLE") {
if (isBiometricsValidType(biometricsType)) {
// If biometric recognition is available, navigate to the Fingerprint
// Screen and wait for the user to press "Continue". Otherwise the whole
// step is bypassed
yield put(
navigateToOnboardingFingerprintScreenAction({
biometryType: biometryTypeOrUnsupportedReason
biometryType: biometricsType
})
);

Expand Down
34 changes: 19 additions & 15 deletions ts/screens/modal/IdentificationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import BaseScreenComponent, {
} from "../../components/screens/BaseScreenComponent";
import I18n from "../../i18n";
import { IdentificationLockModal } from "../../screens/modal/IdentificationLockModal";
import { BiometryPrintableSimpleType } from "../../screens/onboarding/FingerprintScreen";
import {
identificationCancel,
identificationFailure,
Expand All @@ -36,8 +35,10 @@ import customVariables from "../../theme/variables";
import { setAccessibilityFocus } from "../../utils/accessibility";
import {
biometricAuthenticationRequest,
getFingerprintSettings
} from "../../utils/biometric";
BiometricsValidType,
getBiometricsType,
isBiometricsValidType
} from "../../utils/biometrics";
import { maybeNotNullyString } from "../../utils/strings";

type Props = ReturnType<typeof mapDispatchToProps> &
Expand All @@ -56,7 +57,7 @@ type IdentificationByBiometryState = "unstarted" | "failure";
type State = {
identificationByPinState: IdentificationByPinState;
identificationByBiometryState: IdentificationByBiometryState;
biometryType?: BiometryPrintableSimpleType;
biometryType?: BiometricsValidType;
biometryAuthAvailable: boolean;
canInsertPinTooManyAttempts: boolean;
countdown?: Millisecond;
Expand Down Expand Up @@ -135,7 +136,6 @@ class IdentificationModal extends React.PureComponent<Props, State> {

/**
* Activate the interval check on the pin state if the condition is satisfied
* @param remainingAttempts
*/
private scheduleCanInsertPinUpdate = () => {
this.props.identificationFailState.map(failState => {
Expand All @@ -154,11 +154,12 @@ class IdentificationModal extends React.PureComponent<Props, State> {
const { isFingerprintEnabled } = this.props;
setAccessibilityFocus(this.headerRef);
if (isFingerprintEnabled) {
getFingerprintSettings().then(
biometryType =>
getBiometricsType().then(
biometricsType =>
this.setState({
biometryType:
biometryType !== "UNAVAILABLE" ? biometryType : undefined
biometryType: isBiometricsValidType(biometricsType)
? biometricsType
: undefined
}),
_ => 0
);
Expand Down Expand Up @@ -203,14 +204,15 @@ class IdentificationModal extends React.PureComponent<Props, State> {
}
// Check for global properties to know if biometric recognition is enabled
if (isFingerprintEnabled) {
getFingerprintSettings()
getBiometricsType()
.then(
biometryType => {
biometricsType => {
if (updateBiometrySupportProp) {
this.setState({
biometryType:
biometryType !== "UNAVAILABLE" ? biometryType : undefined,
biometryAuthAvailable: biometryType !== "UNAVAILABLE"
biometryType: isBiometricsValidType(biometricsType)
? biometricsType
: undefined,
biometryAuthAvailable: isBiometricsValidType(biometricsType)
});
}
},
Expand All @@ -219,7 +221,9 @@ class IdentificationModal extends React.PureComponent<Props, State> {
.then(
() => {
if (this.state.biometryType) {
this.onFingerprintRequest(this.onIdentificationSuccessHandler);
void this.onFingerprintRequest(
this.onIdentificationSuccessHandler
);
}
},
_ => undefined
Expand Down
71 changes: 32 additions & 39 deletions ts/screens/onboarding/FingerprintScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,45 @@ import { ScreenContentHeader } from "../../components/screens/ScreenContentHeade
import TopScreenComponent from "../../components/screens/TopScreenComponent";
import FooterWithButtons from "../../components/ui/FooterWithButtons";
import I18n from "../../i18n";
import { BiometrySimpleType } from "../../sagas/startup/checkAcknowledgedFingerprintSaga";
import {
abortOnboarding,
fingerprintAcknowledge
} from "../../store/actions/onboarding";
import { Dispatch } from "../../store/actions/types";
import { BiometricsValidType } from "../../utils/biometrics";

type NavigationParams = Readonly<{
biometryType: BiometrySimpleType;
biometryType: BiometricsValidType;
}>;

export type BiometryPrintableSimpleType = "BIOMETRICS" | "TOUCH_ID" | "FACE_ID";
/**
* Print the only BiometrySimplePrintableType values that are passed to the UI
*/
function localizeBiometricsType(
biometryPrintableSimpleType: BiometricsValidType
): string {
switch (biometryPrintableSimpleType) {
case "BIOMETRICS":
return I18n.t("onboarding.fingerprint.body.enrolledType.fingerprint");
case "FACE_ID":
return I18n.t("onboarding.fingerprint.body.enrolledType.faceId");
case "TOUCH_ID":
return I18n.t("onboarding.fingerprint.body.enrolledType.touchId");
}
}

/**
* Print the icon according to current biometry status
*/
function getBiometryIconName(biometryType: BiometricsValidType): string {
switch (biometryType) {
case "FACE_ID":
return "io-face-id";
case "BIOMETRICS":
case "TOUCH_ID":
return "io-fingerprint";
}
}

type Props = NavigationInjectedProps<NavigationParams> &
ReturnType<typeof mapDispatchToProps>;
Expand All @@ -34,38 +61,6 @@ const contextualHelpMarkdown: ContextualHelpPropsMarkdown = {
* the instruction to enable the fingerprint/faceID usage
*/
class FingerprintScreen extends React.PureComponent<Props> {
/**
* Print the only BiometrySimplePrintableType values that are passed to the UI
* @param biometrySimplePrintableType
*/
private renderBiometryType(
biometryPrintableSimpleType: BiometryPrintableSimpleType
): string {
switch (biometryPrintableSimpleType) {
case "BIOMETRICS":
return I18n.t("onboarding.fingerprint.body.enrolledType.fingerprint");
case "FACE_ID":
return I18n.t("onboarding.fingerprint.body.enrolledType.faceId");
case "TOUCH_ID":
return I18n.t("onboarding.fingerprint.body.enrolledType.touchId");
}
}

/**
* Print the icon according to current biometry status
* @param biometrySimplePrintableType
*/
private getBiometryIconName(biometryType: BiometrySimpleType) {
switch (biometryType) {
case "FACE_ID":
return "io-face-id";
case "BIOMETRICS":
case "TOUCH_ID":
case "UNAVAILABLE":
return "io-fingerprint";
}
}

private handleGoBack = () =>
Alert.alert(
I18n.t("onboarding.alert.title"),
Expand Down Expand Up @@ -95,14 +90,12 @@ class FingerprintScreen extends React.PureComponent<Props> {
>
<ScreenContentHeader
title={I18n.t("onboarding.fingerprint.title")}
iconFont={{ name: this.getBiometryIconName(biometryType) }}
iconFont={{ name: getBiometryIconName(biometryType) }}
/>
<Content>
<Text>
{I18n.t("onboarding.fingerprint.body.enrolledText", {
biometryType: this.renderBiometryType(
biometryType as BiometryPrintableSimpleType
)
biometryType: localizeBiometricsType(biometryType)
})}
</Text>
</Content>
Expand Down
15 changes: 7 additions & 8 deletions ts/screens/profile/SecurityScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import { identificationRequest } from "../../store/actions/identification";
import { shufflePinPadOnPayment } from "../../config";
import {
biometricAuthenticationRequest,
getFingerprintSettings
} from "../../utils/biometric";
getBiometricsType,
isBiometricsValidType
} from "../../utils/biometrics";
import { showToast } from "../../utils/showToast";
import { preferenceFingerprintIsEnabledSaveSuccess } from "../../store/actions/persistedPreferences";
import { useScreenReaderEnabled } from "../../utils/accessibility";
Expand All @@ -36,11 +37,9 @@ const SecurityScreen: FC<Props> = ({
const [isFingerprintAvailable, setIsFingerprintAvailable] = useState(false);

useEffect(() => {
getFingerprintSettings().then(
biometryTypeOrUnsupportedReason => {
setIsFingerprintAvailable(
biometryTypeOrUnsupportedReason !== "UNAVAILABLE"
);
getBiometricsType().then(
biometricsType => {
setIsFingerprintAvailable(isBiometricsValidType(biometricsType));
},
_ => undefined
);
Expand All @@ -56,7 +55,7 @@ const SecurityScreen: FC<Props> = ({
return;
}
// if user asks to disable biometric recnognition is required to proceed
biometricAuthenticationRequest(
void biometricAuthenticationRequest(
() => setFingerprintPreference(biometricPreference),
_ =>
showToast(
Expand Down
Loading

0 comments on commit 22a1e49

Please sign in to comment.