Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions src/navigation/onboarding/screens/Pin.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {NativeStackScreenProps} from '@react-navigation/native-stack';
import React, {useLayoutEffect, useRef} from 'react';
import React, {useLayoutEffect, useRef, useState} from 'react';
import {ScrollView} from 'react-native';
import ReactNativeBiometrics, {BiometryTypes} from 'react-native-biometrics';
import {useAndroidBackHandler} from 'react-navigation-backhandler';
Expand All @@ -25,6 +25,8 @@ import {OnboardingGroupParamList, OnboardingScreens} from '../OnboardingGroup';
import {OnboardingImage} from '../components/Containers';
import {useTranslation} from 'react-i18next';
import {Analytics} from '../../../store/analytics/analytics.effects';
import {sleep} from '../../../utils/helper-methods';
import EnableLockWarningModal from '../../tabs/settings/security/components/EnableLockWarningModal';

const PinImage = {
light: (
Expand Down Expand Up @@ -58,6 +60,8 @@ const PinScreen = ({
const dispatch = useAppDispatch();
const logger = useLogger();
const themeType = useThemeType();
const [lockWarningModalVisible, setLockWarningModalVisible] = useState(false);
const shouldEnableBiometricRef = useRef(false);

useAndroidBackHandler(() => true);

Expand Down Expand Up @@ -96,6 +100,10 @@ const PinScreen = ({
dispatch(AppActions.showPinModal({type: 'set', context: 'onboarding'}));
};

const hideLockWarningModal = () => {
setLockWarningModalVisible(false);
};

const onSetBiometricPress = async () => {
try {
haptic('impactLight');
Expand Down Expand Up @@ -128,6 +136,27 @@ const PinScreen = ({
}
};

const onPressBiometric = () => {
haptic('impactLight');
shouldEnableBiometricRef.current = true;
setLockWarningModalVisible(true);
};

const onPressLockWarningConfirm = async () => {
if (!shouldEnableBiometricRef.current) {
return;
}
hideLockWarningModal();
await sleep(500);
shouldEnableBiometricRef.current = false;
await onSetBiometricPress();
};

const onCloseLockWarningModal = () => {
shouldEnableBiometricRef.current = false;
hideLockWarningModal();
};

return (
<PinContainer testID="security-view">
<ScrollView
Expand Down Expand Up @@ -163,7 +192,7 @@ const PinScreen = ({
<Button
testID="biometric-button"
accessibilityLabel="Set biometric login"
onPress={() => onSetBiometricPress()}
onPress={onPressBiometric}
buttonStyle={'secondary'}>
{t('Biometric')}
</Button>
Expand All @@ -178,6 +207,11 @@ const PinScreen = ({
</Button>
</ActionContainer>
</CtaContainer>
<EnableLockWarningModal
isVisible={lockWarningModalVisible}
onBackdropPress={onCloseLockWarningModal}
onConfirm={onPressLockWarningConfirm}
/>
</ScrollView>
</PinContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import React from 'react';
import BaseModal from '../../../../../components/modal/base/BaseModal';
import Button from '../../../../../components/button/Button';
import {useTheme} from '@react-navigation/native';
import {
ActiveOpacity,
WIDTH,
} from '../../../../../components/styled/Containers';
import {BaseText, Paragraph} from '../../../../../components/styled/Text';
import {
Action,
CharcoalBlack,
GhostWhite,
LightBlack,
LightBlue,
NeutralSlate,
SlateDark,
Black,
White,
} from '../../../../../styles/colors';
import {TouchableOpacity} from '@components/base/TouchableOpacity';
import {useTranslation} from 'react-i18next';
import styled from 'styled-components/native';
import CloseModal from '../../../../../../assets/img/close-modal-icon.svg';

const CARD_WIDTH = 343;

interface EnableLockWarningModalProps {
isVisible: boolean;
onBackdropPress: () => void;
onConfirm: () => void;
}

const ModalBackdropContainer = styled.View`
flex: 1;
justify-content: center;
align-items: center;
`;

const ModalCard = styled.View`
width: ${CARD_WIDTH}px;
max-width: ${WIDTH - 32}px;
min-height: 540px;
border-radius: 16px;
padding: 16px;
background-color: ${({theme: {dark}}) => (dark ? CharcoalBlack : GhostWhite)};
`;

const HeaderRow = styled.View`
flex-direction: row;
justify-content: flex-end;
margin-bottom: 24px;
`;

const CloseButton = styled(TouchableOpacity)`
width: 40px;
height: 40px;
border-radius: 20px;
align-items: center;
justify-content: center;
background-color: ${({theme: {dark}}) => (dark ? LightBlack : NeutralSlate)};
`;

const CardBody = styled.View`
flex: 1;
justify-content: space-between;
`;

const TopSection = styled.View`
gap: 32px;
width: 100%;
`;

const ContentSection = styled.View`
width: 100%;
`;

const Title = styled(BaseText)`
color: ${({theme: {dark}}) => (dark ? White : CharcoalBlack)};
font-size: 51px;
line-height: 48px;
letter-spacing: -0.34px;
font-weight: 400;
`;

const AccentTitle = styled(Title)`
color: ${Action};
`;

const Subheading = styled(BaseText)`
font-size: 20px;
line-height: 30px;
font-weight: 600;
color: ${({theme: {dark}}) => (dark ? White : CharcoalBlack)};
`;

const Description = styled(Paragraph)`
color: ${({theme: {dark}}) => (dark ? White : SlateDark)};
margin-top: 8px;
`;

const NoteContainer = styled.View`
background-color: ${({theme: {dark}}) => (dark ? `${Action}40` : LightBlue)};
border-radius: 16px;
padding: 12px 16px 15px;
margin-top: 16px;
`;

const NoteText = styled(BaseText)`
color: ${({theme: {dark}}) => (dark ? White : SlateDark)};
font-size: 13px;
line-height: 20px;
`;

const NoteLabel = styled(NoteText)`
font-weight: 700;
`;

const FooterRow = styled.View`
flex-direction: row;
justify-content: flex-end;
align-items: center;
`;

const EnableLockWarningModal: React.FC<EnableLockWarningModalProps> = ({
isVisible,
onBackdropPress,
onConfirm,
}) => {
const {t} = useTranslation();
const {dark} = useTheme();

return (
<BaseModal
id={'inAppMessage'}
isVisible={isVisible}
backdropOpacity={0.6}
animationIn={'fadeIn'}
animationOut={'fadeOut'}
backdropTransitionOutTiming={0}
hideModalContentWhileAnimating={true}
useNativeDriverForBackdrop={true}
useNativeDriver={true}
style={{margin: 0, alignItems: 'center', justifyContent: 'center'}}
onBackdropPress={onBackdropPress}>
<ModalBackdropContainer>
<ModalCard>
<HeaderRow>
<CloseButton
activeOpacity={ActiveOpacity}
onPress={onBackdropPress}>
<CloseModal width={24} height={24} color={dark ? White : Black} />
</CloseButton>
</HeaderRow>
<CardBody>
<TopSection>
<Title>
{t('Enable')}
{'\n'}
<AccentTitle>{t('biometrics')}</AccentTitle>
</Title>
<ContentSection>
<Subheading>{t('Use with care.')}</Subheading>
<Description>
{t(
'Device passcode may also unlock the app, depending on your device settings. Anyone with biometric credentials enrolled on your device can also access the app. If your device passcode is known, that person can access it as well.',
)}
</Description>
</ContentSection>
</TopSection>
<FooterRow>
<Button
onPress={onConfirm}
backgroundColor={Action}
borderRadius={8}
height={50}
style={{minWidth: 154}}>
{t('I understand')}
</Button>
</FooterRow>
</CardBody>
</ModalCard>
</ModalBackdropContainer>
</BaseModal>
);
};

export default EnableLockWarningModal;
37 changes: 31 additions & 6 deletions src/navigation/tabs/settings/security/screens/SecurityHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
} from '../../../settings/security/SecurityGroup';
import {BitpayIdScreens} from '../../../../bitpay-id/BitpayIdGroup';
import {useNavigation} from '@react-navigation/native';
import EnableLockWarningModal from '../components/EnableLockWarningModal';

export type SecurityHomeProps = NativeStackScreenProps<
SecurityGroupParamList,
Expand Down Expand Up @@ -102,7 +103,9 @@ const SecurityHome: React.FC<SecurityHomeProps> = ({navigation}) => {
const dispatch = useDispatch();
const themeType = useThemeType();
const logger = useLogger();
const [modalVisible, setModalVisible] = useState(false);
const [lockWarningModalVisible, setLockWarningModalVisible] = useState(false);
const [lockOptionsModalVisible, setLockOptionsModalVisible] = useState(false);
const [pendingWarningConfirm, setPendingWarningConfirm] = useState(false);
const passkeySupported = usePasskeySupport();
const user = useSelector<RootState, User | null>(
({APP, BITPAY_ID}) => BITPAY_ID.user[APP.network],
Expand All @@ -113,7 +116,12 @@ const SecurityHome: React.FC<SecurityHomeProps> = ({navigation}) => {
({APP}: RootState) => APP.biometricLockActive,
);
const hideModal = () => {
setModalVisible(false);
setLockOptionsModalVisible(false);
};

const hideLockWarningModal = () => {
setLockWarningModalVisible(false);
setPendingWarningConfirm(false);
};

const setPin = () => {
Expand Down Expand Up @@ -158,7 +166,7 @@ const SecurityHome: React.FC<SecurityHomeProps> = ({navigation}) => {
showBottomNotificationModal(
BiometricErrorNotification(errMsg, async () => {
await sleep(500); // wait for error modal to close before reopening this modal
setModalVisible(true);
setLockOptionsModalVisible(true);
}),
),
);
Expand Down Expand Up @@ -195,7 +203,18 @@ const SecurityHome: React.FC<SecurityHomeProps> = ({navigation}) => {
);
return;
}
setModalVisible(true);
setLockOptionsModalVisible(true);
};

const onPressLockWarningConfirm = async () => {
if (!pendingWarningConfirm) {
return;
}

hideLockWarningModal();
setPendingWarningConfirm(false);
await sleep(500); // wait for the warning modal to close before opening biometrics
setBiometric();
};

const setLockOption = async (
Expand All @@ -206,7 +225,8 @@ const SecurityHome: React.FC<SecurityHomeProps> = ({navigation}) => {
case 'fingerprint':
case 'face':
await sleep(500); // avoid modal conflicting with options sheet or error sheet
setBiometric();
setPendingWarningConfirm(true);
setLockWarningModalVisible(true);
break;

case 'pin':
Expand Down Expand Up @@ -247,9 +267,14 @@ const SecurityHome: React.FC<SecurityHomeProps> = ({navigation}) => {
</Button>
</Setting>
</SettingsComponent>
<EnableLockWarningModal
isVisible={lockWarningModalVisible}
onBackdropPress={hideLockWarningModal}
onConfirm={onPressLockWarningConfirm}
/>
<SheetModal
modalLibrary={'bottom-sheet'}
isVisible={modalVisible}
isVisible={lockOptionsModalVisible}
onBackdropPress={hideModal}>
<SheetContainer>
<Header>
Expand Down
Loading