Skip to content

Commit

Permalink
[#172076080] Added delay between incorrect pin insertion attempts (#1711
Browse files Browse the repository at this point in the history
)

* change to Identification state to support fail data

* serialize/deserialize Date from persistent store

* lint fix

* draft force Logout

* draft IdentificationLockModal

* display remaining attempts

* link forcelogout with identification saga

* add localization

* styling for IdentificationLockModal

* restore deltaTimespanBetweenAttempts = 30

* don't increase failCounter when biometric fail

* fix unused handler

* change countdown rendering and representation as Option

* revert to optional ? props

* identificationfailselector

* update locales

* fromnullable for dateisotransformer and seconds for identification reducer

* fix setInterval method always executed

* display the remaining attempts only if start to lock the application for too many attempts

* fix lint

* renaming variable canInsertPinBiometry to biometryAuthNotAvailable

* revert logic and name of biometryAuthAvailable

* replace map.getorelse with fold

* fix compile error

* don't use BiometryAuth if the app is in timeout for too many attempts

* add comment

Co-authored-by: Matteo Boschi <[email protected]>
  • Loading branch information
fabriziofff and Undermaken authored May 13, 2020
1 parent 66a81fd commit eb86a36
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 54 deletions.
6 changes: 6 additions & 0 deletions locales/en/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,12 @@ identification:
title: Identification required
sensorDescription: Touch sensor
fallbackLabel: Use the unlock code
fail:
wrongCode: Wrong code
remainingAttempts: You have {{attempts}} attempts left.
remainingAttemptSingle: You have {{attempts}} attempt left.
tooManyAttempts: Too many attempts.
waitMessage: "Try again in:"
calendarEvents:
calendarSelect: In which calendar you want to add the event?
removeRequest:
Expand Down
6 changes: 6 additions & 0 deletions locales/it/index.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,12 @@ identification:
title: Identificazione richiesta
sensorDescription: Tocca il sensore
fallbackLabel: Usa il codice di sblocco
fail:
wrongCode: Codice errato
remainingAttempts: Ti rimangono {{attempts}} tentativi.
remainingAttemptSingle: Ti rimane {{attempts}} tentativo.
tooManyAttempts: Troppi tentativi di inserimento errati.
waitMessage: "Riprova tra:"
calendarEvents:
calendarSelect: In quale calendario vuoi aggiungere l'evento?
removeRequest:
Expand Down
194 changes: 158 additions & 36 deletions ts/IdentificationModal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Millisecond } from "italia-ts-commons/lib/units";
import { Content, Text, View } from "native-base";
import * as React from "react";
import { Alert, Modal, StatusBar, StyleSheet } from "react-native";
Expand All @@ -16,14 +17,22 @@ import { BiometryPrintableSimpleType } from "./screens/onboarding/FingerprintScr
import {
identificationCancel,
identificationFailure,
identificationForceLogout,
identificationPinReset,
identificationSuccess
} from "./store/actions/identification";
import { ReduxProps } from "./store/actions/types";
import {
freeAttempts,
identificationFailSelector,
maxAttempts
} from "./store/reducers/identification";
import { GlobalState } from "./store/reducers/types";
import variables from "./theme/variables";
import { authenticateConfig } from "./utils/biometric";

import { IdentificationLockModal } from "./screens/modal/IdentificationLockModal";

type Props = ReturnType<typeof mapStateToProps> & ReduxProps;

/**
Expand All @@ -40,14 +49,21 @@ type State = {
identificationByPinState: IdentificationByPinState;
identificationByBiometryState: IdentificationByBiometryState;
biometryType?: BiometryPrintableSimpleType;
canInsertPin: boolean;
biometryAuthAvailable: boolean;
canInsertPinTooManyAttempts: boolean;
countdown?: Millisecond;
};

const contextualHelpMarkdown: ContextualHelpPropsMarkdown = {
title: "onboarding.pin.contextualHelpTitle",
body: "onboarding.pin.contextualHelpContent"
};

const checkPinInterval = 100 as Millisecond;

// the threshold of attempts after which it is necessary to activate the timer check
const checkTimerThreshold = maxAttempts - freeAttempts;

const renderIdentificationByPinState = (
identificationByPinState: IdentificationByPinState
) => {
Expand Down Expand Up @@ -111,10 +127,47 @@ class IdentificationModal extends React.PureComponent<Props, State> {
this.state = {
identificationByPinState: "unstarted",
identificationByBiometryState: "unstarted",
canInsertPin: false
biometryAuthAvailable: true,
canInsertPinTooManyAttempts: this.props.identificationFailState.isNone()
};
}

private idUpdateCanInsertPinTooManyAttempts?: number;

/**
* Update the state using the actual props value of the `identificationFailState`
* return the updated value of `canInsertPinTooManyAttempts` in order to be used without waiting the state update
*/
private updateCanInsertPinTooManyAttempts = () => {
return this.props.identificationFailState.map(errorData => {
const now = new Date();
const canInsertPinTooManyAttempts = errorData.nextLegalAttempt <= now;
this.setState({
canInsertPinTooManyAttempts,
countdown: (errorData.nextLegalAttempt.getTime() -
now.getTime()) as Millisecond
});
return canInsertPinTooManyAttempts;
});
};

/**
* Activate the interval check on the pin state if the condition is satisfied
* @param remainingAttempts
*/
private scheduleCanInsertPinUpdate = () => {
this.props.identificationFailState.map(failState => {
if (failState.remainingAttempts < checkTimerThreshold) {
this.updateCanInsertPinTooManyAttempts();
// tslint:disable-next-line: no-object-mutation
this.idUpdateCanInsertPinTooManyAttempts = setInterval(
this.updateCanInsertPinTooManyAttempts,
checkPinInterval
);
}
});
};

public componentDidMount() {
const { isFingerprintEnabled } = this.props;
if (isFingerprintEnabled) {
Expand All @@ -130,8 +183,19 @@ class IdentificationModal extends React.PureComponent<Props, State> {
);
} else {
// if the biometric is not available unlock the unlock code insertion
this.setState({ canInsertPin: true });
this.setState({ biometryAuthAvailable: false });
}

// first time the component is mounted, need to calculate the state value for `canInsertPinTooManyAttempts`
// and schedule the update if needed
this.updateCanInsertPinTooManyAttempts().map(_ =>
this.scheduleCanInsertPinUpdate()
);
}

// atm this method is never called because the component won't be never unmount
public componentWillUnmount() {
clearInterval(this.idUpdateCanInsertPinTooManyAttempts);
}

/**
Expand All @@ -151,9 +215,9 @@ class IdentificationModal extends React.PureComponent<Props, State> {
updateBiometrySupportProp: boolean;
}) {
// check if the state of identification process is correct
const { identificationState, isFingerprintEnabled } = this.props;
const { identificationProgressState, isFingerprintEnabled } = this.props;

if (identificationState.kind !== "started") {
if (identificationProgressState.kind !== "started") {
return;
}

Expand All @@ -169,9 +233,9 @@ class IdentificationModal extends React.PureComponent<Props, State> {
biometryType !== "UNAVAILABLE"
? biometryType
: undefined,
canInsertPin:
biometryType === "NOT_ENROLLED" ||
biometryType === "UNAVAILABLE"
biometryAuthAvailable:
biometryType !== "NOT_ENROLLED" &&
biometryType !== "UNAVAILABLE"
});
}
},
Expand All @@ -180,42 +244,75 @@ class IdentificationModal extends React.PureComponent<Props, State> {
.then(
() => {
if (this.state.biometryType) {
this.onFingerprintRequest(
this.onIdentificationSuccessHandler,
this.onIdentificationFailureHandler
);
this.onFingerprintRequest(this.onIdentificationSuccessHandler);
}
},
_ => undefined
);
}
}

public componentDidUpdate(prevProps: Props) {
public componentDidUpdate(prevProps: Props, prevState: State) {
// When app becomes active from background the state of TouchID support
// must be updated, because it might be switched off.
// Don't do this check if I can't authenticate for too many attempts (canInsertPinTooManyAttempts === false)
if (
(prevProps.appState === "background" &&
this.state.canInsertPinTooManyAttempts &&
((prevProps.appState === "background" &&
this.props.appState === "active") ||
(prevProps.identificationState.kind !== "started" &&
this.props.identificationState.kind === "started")
(prevProps.identificationProgressState.kind !== "started" &&
this.props.identificationProgressState.kind === "started"))
) {
this.maybeTriggerFingerprintRequest({
updateBiometrySupportProp:
prevProps.appState !== "active" && this.props.appState === "active"
});
}

const previousAttempts = prevProps.identificationFailState.fold(
Number.MAX_VALUE,
x => x.remainingAttempts
);

const currentAttempts = this.props.identificationFailState.fold(
Number.MAX_VALUE,
x => x.remainingAttempts
);

// trigger an update in the management of the updateInterval if the attempts or the state
// `canInsertPinTooManyAttempts` is changed
if (
previousAttempts !== currentAttempts ||
prevState.canInsertPinTooManyAttempts !==
this.state.canInsertPinTooManyAttempts
) {
// trigger a state update based on the current props and use the results to choose what to do
// with the scheduled interval
const caninsertPin = this.updateCanInsertPinTooManyAttempts().getOrElse(
true
);
// if the pin can be inserted, the timer is no longer needed
if (caninsertPin) {
clearInterval(this.idUpdateCanInsertPinTooManyAttempts);
// tslint:disable-next-line: no-object-mutation
this.idUpdateCanInsertPinTooManyAttempts = undefined;

// if the pin can't be inserted and is not scheduled an interval, schedule an update
} else if (this.idUpdateCanInsertPinTooManyAttempts === undefined) {
this.scheduleCanInsertPinUpdate();
}
}
}

private onIdentificationSuccessHandler = () => {
const { identificationState, dispatch } = this.props;
const { identificationProgressState, dispatch } = this.props;

if (identificationState.kind !== "started") {
if (identificationProgressState.kind !== "started") {
return;
}

// The identification state is started we need to show the modal
const { identificationSuccessData } = identificationState;
const { identificationSuccessData } = identificationProgressState;

if (identificationSuccessData) {
identificationSuccessData.onSuccess();
Expand All @@ -224,14 +321,26 @@ class IdentificationModal extends React.PureComponent<Props, State> {
};

private onIdentificationFailureHandler = () => {
const { dispatch } = this.props;
dispatch(identificationFailure());
const { dispatch, identificationFailState } = this.props;

const forceLogout = identificationFailState
.map(failState => failState.remainingAttempts === 1)
.getOrElse(false);
if (forceLogout) {
dispatch(identificationForceLogout());
} else {
dispatch(identificationFailure());
}
};

public render() {
const { identificationState, isFingerprintEnabled, dispatch } = this.props;
const {
identificationProgressState,
isFingerprintEnabled,
dispatch
} = this.props;

if (identificationState.kind !== "started") {
if (identificationProgressState.kind !== "started") {
return null;
}

Expand All @@ -242,18 +351,32 @@ class IdentificationModal extends React.PureComponent<Props, State> {
identificationGenericData,
identificationCancelData,
shufflePad
} = identificationState;
} = identificationProgressState;

const {
identificationByPinState,
identificationByBiometryState,
biometryType
biometryType,
countdown
} = this.state;

const identificationMessage = identificationGenericData
? identificationGenericData.message
: this.renderBiometryType();

const canInsertPin =
!this.state.biometryAuthAvailable &&
this.state.canInsertPinTooManyAttempts;

// display the remaining attempts number only if start to lock the application for too many attempts
const displayRemainingAttempts = this.props.identificationFailState.fold(
undefined,
failState =>
failState.remainingAttempts <= maxAttempts - freeAttempts
? failState.remainingAttempts
: undefined
);

/**
* Create handlers merging default internal actions (to manage the identification state)
* with, if available, custom actions passed as props.
Expand All @@ -269,7 +392,9 @@ class IdentificationModal extends React.PureComponent<Props, State> {
dispatch(identificationPinReset());
};

return (
return !this.state.canInsertPinTooManyAttempts ? (
IdentificationLockModal({ countdown })
) : (
<Modal onRequestClose={onRequestCloseHandler}>
<BaseScreenComponent
primary={true}
Expand All @@ -294,13 +419,10 @@ class IdentificationModal extends React.PureComponent<Props, State> {
isFingerprintEnabled={isFingerprintEnabled}
biometryType={biometryType}
onFingerPrintReq={() =>
this.onFingerprintRequest(
this.onIdentificationSuccessHandler,
this.onIdentificationFailureHandler
)
this.onFingerprintRequest(this.onIdentificationSuccessHandler)
}
shufflePad={shufflePad}
disabled={!this.state.canInsertPin}
disabled={!canInsertPin}
compareWithCode={pin as string}
activeColor={"white"}
inactiveColor={"white"}
Expand All @@ -320,6 +442,7 @@ class IdentificationModal extends React.PureComponent<Props, State> {
? onIdentificationCancelHandler
: undefined
}
remainingAttempts={displayRemainingAttempts}
/>
{renderIdentificationByPinState(identificationByPinState)}
{renderIdentificationByBiometryState(identificationByBiometryState)}
Expand Down Expand Up @@ -369,8 +492,7 @@ class IdentificationModal extends React.PureComponent<Props, State> {
};

private onFingerprintRequest = (
onIdentificationSuccessHandler: () => void,
onIdentificationFailureHandler: () => void
onIdentificationSuccessHandler: () => void
) => {
TouchID.authenticate(
I18n.t("identification.biometric.popup.reason"),
Expand All @@ -385,7 +507,7 @@ class IdentificationModal extends React.PureComponent<Props, State> {
.catch((error: AuthenticationError) => {
// some error occured, enable pin insertion
this.setState({
canInsertPin: true
biometryAuthAvailable: false
});
if (isDebugBiometricIdentificationEnabled) {
Alert.alert("identification.biometric.title", `KO: ${error.code}`);
Expand All @@ -398,13 +520,13 @@ class IdentificationModal extends React.PureComponent<Props, State> {
identificationByBiometryState: "failure"
});
}
onIdentificationFailureHandler();
});
};
}

const mapStateToProps = (state: GlobalState) => ({
identificationState: state.identification,
identificationProgressState: state.identification.progress,
identificationFailState: identificationFailSelector(state),
isFingerprintEnabled: state.persistedPreferences.isFingerprintEnabled,
appState: state.appState.appState
});
Expand Down
Loading

0 comments on commit eb86a36

Please sign in to comment.