diff --git a/src/navigation/onboarding/screens/Pin.tsx b/src/navigation/onboarding/screens/Pin.tsx
index 102406e234..8d5574eded 100644
--- a/src/navigation/onboarding/screens/Pin.tsx
+++ b/src/navigation/onboarding/screens/Pin.tsx
@@ -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';
@@ -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: (
@@ -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);
@@ -96,6 +100,10 @@ const PinScreen = ({
dispatch(AppActions.showPinModal({type: 'set', context: 'onboarding'}));
};
+ const hideLockWarningModal = () => {
+ setLockWarningModalVisible(false);
+ };
+
const onSetBiometricPress = async () => {
try {
haptic('impactLight');
@@ -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 (
onSetBiometricPress()}
+ onPress={onPressBiometric}
buttonStyle={'secondary'}>
{t('Biometric')}
@@ -178,6 +207,11 @@ const PinScreen = ({
+
);
diff --git a/src/navigation/tabs/settings/security/components/EnableLockWarningModal.tsx b/src/navigation/tabs/settings/security/components/EnableLockWarningModal.tsx
new file mode 100644
index 0000000000..893fca1c81
--- /dev/null
+++ b/src/navigation/tabs/settings/security/components/EnableLockWarningModal.tsx
@@ -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 = ({
+ isVisible,
+ onBackdropPress,
+ onConfirm,
+}) => {
+ const {t} = useTranslation();
+ const {dark} = useTheme();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {t('Enable')}
+ {'\n'}
+ {t('biometrics')}
+
+
+ {t('Use with care.')}
+
+ {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.',
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default EnableLockWarningModal;
diff --git a/src/navigation/tabs/settings/security/screens/SecurityHome.tsx b/src/navigation/tabs/settings/security/screens/SecurityHome.tsx
index 67a90b60db..c93ac6ddd8 100644
--- a/src/navigation/tabs/settings/security/screens/SecurityHome.tsx
+++ b/src/navigation/tabs/settings/security/screens/SecurityHome.tsx
@@ -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,
@@ -102,7 +103,9 @@ const SecurityHome: React.FC = ({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(
({APP, BITPAY_ID}) => BITPAY_ID.user[APP.network],
@@ -113,7 +116,12 @@ const SecurityHome: React.FC = ({navigation}) => {
({APP}: RootState) => APP.biometricLockActive,
);
const hideModal = () => {
- setModalVisible(false);
+ setLockOptionsModalVisible(false);
+ };
+
+ const hideLockWarningModal = () => {
+ setLockWarningModalVisible(false);
+ setPendingWarningConfirm(false);
};
const setPin = () => {
@@ -158,7 +166,7 @@ const SecurityHome: React.FC = ({navigation}) => {
showBottomNotificationModal(
BiometricErrorNotification(errMsg, async () => {
await sleep(500); // wait for error modal to close before reopening this modal
- setModalVisible(true);
+ setLockOptionsModalVisible(true);
}),
),
);
@@ -195,7 +203,18 @@ const SecurityHome: React.FC = ({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 (
@@ -206,7 +225,8 @@ const SecurityHome: React.FC = ({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':
@@ -247,9 +267,14 @@ const SecurityHome: React.FC = ({navigation}) => {
+