Skip to content

Commit ff1cb6b

Browse files
committed
fix: reduce PIN attempts in Send and ChangePIN flows
1 parent 08edd84 commit ff1cb6b

File tree

15 files changed

+332
-393
lines changed

15 files changed

+332
-393
lines changed

.eslintrc.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ module.exports = {
100100
additionalHooks: 'useDebouncedEffect',
101101
},
102102
],
103+
104+
// prettier
105+
'no-mixed-spaces-and-tabs': 0,
103106
},
104107
overrides: [
105108
// Disable type-aware linting for .js files

e2e/security.e2e.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,10 @@ d('Settings Security And Privacy', () => {
188188
await element(by.id('UseBiometryInstead')).tap();
189189
await device.matchFace();
190190
await sleep(1000);
191-
await element(by.id('ChangePIN')).tap();
192-
await element(by.id('N1').withAncestor(by.id('PinPad'))).multiTap(4);
193-
await sleep(1000);
191+
await element(by.id('PINChange')).tap();
192+
await element(by.id('N3').withAncestor(by.id('ChangePIN'))).multiTap(4);
193+
await expect(element(by.id('AttemptsRemaining'))).toBeVisible();
194194
await element(by.id('N1').withAncestor(by.id('ChangePIN'))).multiTap(4);
195-
await sleep(1000);
196195
await element(by.id('N2').withAncestor(by.id('ChangePIN2'))).multiTap(4);
197196
await element(by.id('N9').withAncestor(by.id('ChangePIN2'))).multiTap(4);
198197
await expect(element(by.id('WrongPIN'))).toBeVisible();

ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2292,6 +2292,6 @@ SPEC CHECKSUMS:
22922292
Yoga: 2a45d7e59592db061217551fd3bbe2dd993817ae
22932293
ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5
22942294

2295-
PODFILE CHECKSUM: b1ff2276b558626bd07bddd66e26b06f3fc76609
2295+
PODFILE CHECKSUM: cb153cb4a39e6c92c8b869eafab65a4bba7b869f
22962296

22972297
COCOAPODS: 1.15.2

src/components/AuthCheck.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const AuthCheck = ({
4343
<Animated.View style={StyleSheet.absoluteFillObject} exiting={FadeOut}>
4444
<ThemedView style={styles.root}>
4545
<Biometrics
46-
onSuccess={(): void => onSuccess?.()}
46+
onSuccess={onSuccess}
4747
onFailure={(): void => setBioEnabled(false)}
4848
/>
4949
</ThemedView>
@@ -58,7 +58,7 @@ const AuthCheck = ({
5858
showLogoOnPIN={showLogoOnPIN}
5959
allowBiometrics={biometrics && !requirePin}
6060
onShowBiotmetrics={(): void => setBioEnabled(true)}
61-
onSuccess={(): void => onSuccess?.()}
61+
onSuccess={onSuccess}
6262
/>
6363
</Animated.View>
6464
);

src/components/Biometrics.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const Biometrics = ({
4141
style,
4242
children,
4343
}: {
44-
onSuccess: () => void;
44+
onSuccess?: () => void;
4545
onFailure?: () => void;
4646
style?: StyleProp<ViewStyle>;
4747
children?: ReactElement;
@@ -101,7 +101,7 @@ const Biometrics = ({
101101
.then(({ success }) => {
102102
if (success) {
103103
dispatch(updateSettings({ biometrics: true }));
104-
onSuccess();
104+
onSuccess?.();
105105
} else {
106106
vibrate();
107107
onFailure?.();

src/components/PinPad.tsx

Lines changed: 19 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,21 @@
1-
import React, {
2-
memo,
3-
ReactElement,
4-
useState,
5-
useEffect,
6-
useCallback,
7-
} from 'react';
8-
import { StyleSheet, View, Pressable } from 'react-native';
9-
import { FadeIn, FadeOut } from 'react-native-reanimated';
1+
import React, { ReactElement, memo, useEffect, useState } from 'react';
102
import { useTranslation } from 'react-i18next';
3+
import { Pressable, StyleSheet, View } from 'react-native';
4+
import { FadeIn, FadeOut } from 'react-native-reanimated';
115

12-
import { BodyS, Subtitle } from '../styles/text';
13-
import { View as ThemedView, AnimatedView } from '../styles/components';
6+
import BitkitLogo from '../assets/bitkit-logo.svg';
7+
import { PIN_ATTEMPTS } from '../constants/app';
8+
import usePIN from '../hooks/pin';
9+
import { showBottomSheet } from '../store/utils/ui';
10+
import { AnimatedView, View as ThemedView } from '../styles/components';
1411
import { FaceIdIcon, TouchIdIcon } from '../styles/icons';
15-
import SafeAreaInset from './SafeAreaInset';
16-
import NavigationHeader from './NavigationHeader';
12+
import { BodyS, Subtitle } from '../styles/text';
13+
import rnBiometrics from '../utils/biometrics';
1714
import { IsSensorAvailableResult } from './Biometrics';
15+
import NavigationHeader from './NavigationHeader';
1816
import NumberPad from './NumberPad';
17+
import SafeAreaInset from './SafeAreaInset';
1918
import Button from './buttons/Button';
20-
import useColors from '../hooks/colors';
21-
import { wipeApp } from '../store/utils/settings';
22-
import { showBottomSheet } from '../store/utils/ui';
23-
import { vibrate } from '../utils/helpers';
24-
import rnBiometrics from '../utils/biometrics';
25-
import { showToast } from '../utils/notifications';
26-
import { setKeychainValue, getKeychainValue } from '../utils/keychain';
27-
import BitkitLogo from '../assets/bitkit-logo.svg';
28-
import { PIN_ATTEMPTS } from '../constants/app';
2919

3020
const PinPad = ({
3121
showLogoOnPIN,
@@ -37,122 +27,22 @@ const PinPad = ({
3727
showLogoOnPIN: boolean;
3828
allowBiometrics: boolean;
3929
showBackNavigation?: boolean;
40-
onSuccess: () => void;
30+
onSuccess?: () => void;
4131
onShowBiotmetrics?: () => void;
4232
}): ReactElement => {
4333
const { t } = useTranslation('security');
44-
const { brand, brand08 } = useColors();
45-
const [pin, setPin] = useState('');
46-
const [isLoading, setIsLoading] = useState(true);
47-
const [attemptsRemaining, setAttemptsRemaining] = useState(0);
4834
const [biometryData, setBiometricData] = useState<IsSensorAvailableResult>();
49-
50-
const handleOnPress = (key: string): void => {
51-
vibrate();
52-
if (key === 'delete') {
53-
setPin((p) => {
54-
return p.length === 0 ? '' : p.slice(0, -1);
55-
});
56-
} else {
57-
setPin((p) => {
58-
return p.length === 4 ? p : p + key;
59-
});
60-
}
61-
};
62-
63-
// Reduce the amount of pin attempts remaining.
64-
const reducePinAttemptsRemaining = useCallback(async (): Promise<void> => {
65-
const _attemptsRemaining = attemptsRemaining - 1;
66-
await setKeychainValue({
67-
key: 'pinAttemptsRemaining',
68-
value: `${_attemptsRemaining}`,
69-
});
70-
setAttemptsRemaining(_attemptsRemaining);
71-
}, [attemptsRemaining]);
72-
73-
// init view
74-
useEffect(() => {
75-
(async (): Promise<void> => {
76-
const attemptsRemainingResponse = await getKeychainValue({
77-
key: 'pinAttemptsRemaining',
78-
});
79-
80-
if (
81-
!attemptsRemainingResponse.error &&
82-
Number(attemptsRemainingResponse.data) !== Number(attemptsRemaining)
83-
) {
84-
let numAttempts =
85-
attemptsRemainingResponse.data !== undefined
86-
? Number(attemptsRemainingResponse.data)
87-
: 5;
88-
setAttemptsRemaining(numAttempts);
89-
}
90-
})();
91-
}, [attemptsRemaining]);
35+
const { attemptsRemaining, Dots, handleNumberPress, isLastAttempt, loading } =
36+
usePIN(onSuccess);
9237

9338
// on mount
9439
useEffect(() => {
9540
(async (): Promise<void> => {
96-
setIsLoading(true);
97-
// wait for initial keychain read
98-
await getKeychainValue({ key: 'pinAttemptsRemaining' });
99-
// get available biometrics
10041
const data = await rnBiometrics.isSensorAvailable();
10142
setBiometricData(data);
102-
setIsLoading(false);
10343
})();
10444
}, []);
10545

106-
// submit pin
107-
useEffect(() => {
108-
const timer = setTimeout(async () => {
109-
if (pin.length !== 4) {
110-
return;
111-
}
112-
113-
const realPIN = await getKeychainValue({ key: 'pin' });
114-
115-
// error getting pin
116-
if (realPIN.error) {
117-
await reducePinAttemptsRemaining();
118-
vibrate();
119-
setPin('');
120-
return;
121-
}
122-
123-
// incorrect pin
124-
if (pin !== realPIN?.data) {
125-
if (attemptsRemaining <= 1) {
126-
vibrate({ type: 'default' });
127-
await wipeApp();
128-
showToast({
129-
type: 'warning',
130-
title: t('wiped_title'),
131-
description: t('wiped_message'),
132-
});
133-
} else {
134-
await reducePinAttemptsRemaining();
135-
}
136-
137-
vibrate();
138-
setPin('');
139-
return;
140-
}
141-
142-
// correct pin
143-
await setKeychainValue({
144-
key: 'pinAttemptsRemaining',
145-
value: PIN_ATTEMPTS,
146-
});
147-
setPin('');
148-
onSuccess?.();
149-
}, 500);
150-
151-
return (): void => clearTimeout(timer);
152-
}, [pin, attemptsRemaining, onSuccess, reducePinAttemptsRemaining, t]);
153-
154-
const isLastAttempt = attemptsRemaining === 1;
155-
15646
const biometricsName =
15747
biometryData?.biometryType === 'TouchID'
15848
? t('bio_touch_id')
@@ -172,7 +62,7 @@ const PinPad = ({
17262
</View>
17363

17464
<View style={styles.content}>
175-
{!isLoading && (
65+
{!loading && biometryData !== undefined && (
17666
<AnimatedView
17767
style={styles.contentInner}
17868
color="transparent"
@@ -210,40 +100,26 @@ const PinPad = ({
210100
<Button
211101
style={styles.biometrics}
212102
text={t('pin_use_biometrics', { biometricsName })}
103+
onPress={onShowBiotmetrics}
213104
icon={
214105
biometryData?.biometryType === 'FaceID' ? (
215106
<FaceIdIcon height={16} width={16} color="brand" />
216107
) : (
217108
<TouchIdIcon height={16} width={16} color="brand" />
218109
)
219110
}
220-
onPress={onShowBiotmetrics}
221111
/>
222112
)}
223113
</View>
224114

225115
<View style={styles.dots}>
226-
{Array(4)
227-
.fill(null)
228-
.map((_, i) => (
229-
<View
230-
key={i}
231-
style={[
232-
styles.dot,
233-
{
234-
borderColor: brand,
235-
backgroundColor:
236-
pin[i] === undefined ? brand08 : brand,
237-
},
238-
]}
239-
/>
240-
))}
116+
<Dots />
241117
</View>
242118

243119
<NumberPad
244120
style={styles.numberpad}
245121
type="simple"
246-
onPress={handleOnPress}
122+
onPress={handleNumberPress}
247123
/>
248124
</AnimatedView>
249125
)}
@@ -302,13 +178,6 @@ const styles = StyleSheet.create({
302178
marginBottom: 16,
303179
},
304180
dots: {
305-
flexDirection: 'row',
306-
justifyContent: 'center',
307-
marginBottom: 48,
308-
},
309-
dot: {
310-
width: 20,
311-
height: 20,
312181
borderRadius: 10,
313182
marginHorizontal: 12,
314183
borderWidth: 1,

0 commit comments

Comments
 (0)