/** * This is a reactnavigation SCREEN, which we use to set a PIN. */ import { NativeStackScreenProps } from '@react-navigation/native-stack' import * as React from 'react' import { WithTranslation } from 'react-i18next' import { StyleSheet } from 'react-native' import { BIOMETRY_TYPE } from 'react-native-keychain' import { SafeAreaView } from 'react-native-safe-area-context' import { connect } from 'react-redux' import { initializeAccount, setPincodeSuccess } from 'src/account/actions' import { PincodeType } from 'src/account/reducer' import AppAnalytics from 'src/analytics/AppAnalytics' import { OnboardingEvents, SettingsEvents } from 'src/analytics/Events' import { supportedBiometryTypeSelector } from 'src/app/selectors' import DevSkipButton from 'src/components/DevSkipButton' import i18n, { withTranslation } from 'src/i18n' import { HeaderTitleWithSubtitle, nuxNavigationOptions, nuxNavigationOptionsOnboarding, } from 'src/navigator/Headers' import { navigateBack } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' import { getOnboardingStepValues, goToNextOnboardingScreen, OnboardingProps, onboardingPropsSelector, } from 'src/onboarding/steps' import { DEFAULT_CACHE_ACCOUNT, isPinValid, PinBlocklist, updatePin, } from 'src/pincode/authentication' import { getCachedPin, setCachedPin } from 'src/pincode/PasswordCache' import Pincode from 'src/pincode/Pincode' import { RootState } from 'src/redux/reducers' import Colors from 'src/styles/colors' import Logger from 'src/utils/Logger' import { currentAccountSelector } from 'src/web3/selectors' interface StateProps { choseToRestoreAccount: boolean | undefined useExpandedBlocklist: boolean account: string registrationStep: { step: number; totalSteps: number } onboardingProps: OnboardingProps supportedBiometryType: BIOMETRY_TYPE | null } interface DispatchProps { initializeAccount: typeof initializeAccount setPincodeSuccess: typeof setPincodeSuccess } interface State { oldPin: string pin1: string pin2: string errorText: string | null blocklist: PinBlocklist | undefined isVerifying: boolean } type ScreenProps = NativeStackScreenProps type Props = ScreenProps & StateProps & DispatchProps & WithTranslation function mapStateToProps(state: RootState): StateProps { return { choseToRestoreAccount: state.account.choseToRestoreAccount, onboardingProps: onboardingPropsSelector(state), registrationStep: getOnboardingStepValues(Screens.PincodeSet, onboardingPropsSelector(state)), useExpandedBlocklist: state.app.pincodeUseExpandedBlocklist, account: currentAccountSelector(state) ?? '', supportedBiometryType: supportedBiometryTypeSelector(state), } } const mapDispatchToProps = { initializeAccount, setPincodeSuccess, } export class PincodeSet extends React.Component { static navigationOptions = ({ route }: ScreenProps) => { const changePin = route.params?.changePin return { ...(changePin ? nuxNavigationOptions : nuxNavigationOptionsOnboarding), headerTitle: () => ( ), } } state: State = { oldPin: '', pin1: '', pin2: '', errorText: null, blocklist: undefined, isVerifying: false, } componentDidMount = () => { if (this.isChangingPin()) { // We're storing the PIN on the state because it will definitely be in the cache now // but it might expire by the time the user enters their new PIN if they take more // than 5 minutes to do so. this.setState({ oldPin: getCachedPin(DEFAULT_CACHE_ACCOUNT) ?? '' }) } // Load the PIN blocklist from the bundle into the component state. if (this.props.useExpandedBlocklist) { this.setState({ blocklist: new PinBlocklist() }) } // Setting choseToRestoreAccount on route param for navigationOptions this.props.navigation.setParams({ choseToRestoreAccount: this.props.choseToRestoreAccount, registrationStep: this.props.registrationStep, }) } componentDidUpdate(prevProps: Props) { if (prevProps.registrationStep.step !== this.props.registrationStep.step) { this.props.navigation.setParams({ registrationStep: this.props.registrationStep, }) } } isChangingPin() { return this.props.route.params?.changePin } navigateToNextScreen = () => { if (this.isChangingPin()) { navigateBack() } else { goToNextOnboardingScreen({ firstScreenInCurrentStep: Screens.PincodeSet, onboardingProps: this.props.onboardingProps, }) } } onChangePin1 = (pin1: string) => { this.setState({ pin1, errorText: null }) } onChangePin2 = (pin2: string) => { this.setState({ pin2 }) } isPin1Valid = (pin: string) => { return ( isPinValid(pin) && (!this.props.useExpandedBlocklist || this.state.blocklist?.contains(pin) === false) ) } isPin2Valid = (pin: string) => { return this.state.pin1 === pin } onCompletePin1 = () => { if (this.isPin1Valid(this.state.pin1)) { this.setState({ isVerifying: true }) if (this.isChangingPin()) { AppAnalytics.track(SettingsEvents.change_pin_new_pin_entered) } } else { AppAnalytics.track(OnboardingEvents.pin_invalid, { error: 'Pin is invalid' }) if (this.isChangingPin()) { AppAnalytics.track(SettingsEvents.change_pin_new_pin_error) } this.setState({ pin1: '', pin2: '', errorText: this.props.t('pincodeSet.invalidPin'), }) } } onCompletePin2 = async (pin2: string) => { const { pin1 } = this.state if (this.isPin1Valid(pin1) && this.isPin2Valid(pin2)) { if (this.isChangingPin()) { const updated = await updatePin(this.props.account, this.state.oldPin, pin2) if (updated) { AppAnalytics.track(SettingsEvents.change_pin_new_pin_confirmed) Logger.showMessage(this.props.t('pinChanged')) } else { AppAnalytics.track(SettingsEvents.change_pin_new_pin_error) Logger.showMessage(this.props.t('pinChangeFailed')) } } else { this.props.setPincodeSuccess(PincodeType.CustomPin) setCachedPin(DEFAULT_CACHE_ACCOUNT, pin1) AppAnalytics.track(OnboardingEvents.pin_set) } this.navigateToNextScreen() } else { if (this.isChangingPin()) { AppAnalytics.track(SettingsEvents.change_pin_new_pin_error) } AppAnalytics.track(OnboardingEvents.pin_invalid, { error: 'Pins do not match' }) this.setState({ isVerifying: false, pin1: '', pin2: '', errorText: this.props.t('pincodeSet.pinsDontMatch'), }) } } render() { const { t } = this.props const changingPin = this.isChangingPin() const { errorText, isVerifying, pin1, pin2 } = this.state return ( {isVerifying ? ( ) : ( )} ) } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'space-between', paddingTop: 72, }, changePinContainer: { flex: 1, backgroundColor: Colors.backgroundPrimary, justifyContent: 'space-between', }, }) export default connect( mapStateToProps, mapDispatchToProps )(withTranslation()(PincodeSet))