import React from 'react'; import { FormattedMessage as Message, defineMessages } from 'react-intl'; import { Button, FormModel } from 'app/components/ui/form'; import styles from 'app/components/profile/profileForm.scss'; import Stepper from 'app/components/ui/stepper'; import { SlideMotion } from 'app/components/ui/motion'; import { ScrollIntoView } from 'app/components/ui/scroll'; import logger from 'app/services/logger'; import { getSecret, enable as enableMFA } from 'app/services/api/mfa'; import { Form } from 'app/components/ui/form'; import Context from '../Context'; import Instructions from './instructions'; import KeyForm from './keyForm'; import Confirmation from './Confirmation'; const STEPS_TOTAL = 3; export type MfaStep = 0 | 1 | 2; type Props = { onChangeStep: (nextStep: number) => void; confirmationForm: FormModel; onSubmit: (form: FormModel, sendData: () => Promise) => Promise; onComplete: () => void; step: MfaStep; }; const labels = defineMessages({ theAppIsInstalled: 'App has been installed', ready: 'Ready', enable: 'Enable', }); interface State { isLoading: boolean; activeStep: MfaStep; secret: string; qrCodeSrc: string; } export default class MfaEnable extends React.PureComponent { static contextType = Context; declare context: React.ContextType; static defaultProps = { confirmationForm: new FormModel(), step: 0, }; state = { isLoading: false, activeStep: this.props.step, qrCodeSrc: '', secret: '', }; confirmationFormEl: Form | null; componentDidMount() { this.syncState(this.props); } static getDerivedStateFromProps(props: Props, state: State) { if (props.step !== state.activeStep) { return { activeStep: props.step, }; } return null; } componentDidUpdate() { this.syncState(this.props); } render() { const { activeStep, isLoading } = this.state; const stepsData = [ { buttonLabel: labels.theAppIsInstalled, buttonAction: () => this.nextStep(), }, { buttonLabel: labels.ready, buttonAction: () => this.nextStep(), }, { buttonLabel: labels.enable, buttonAction: () => this.confirmationFormEl && this.confirmationFormEl.submit(), }, ]; const { buttonLabel, buttonAction } = stepsData[activeStep]; return (
{activeStep > 0 ? : null} {this.renderStepForms()}
); } renderStepForms() { const { activeStep, secret, qrCodeSrc } = this.state; return ( (this.confirmationFormEl = el)} onSubmit={this.onTotpSubmit} onInvalid={() => this.forceUpdate()} /> ); } syncState(props: Props) { const { isLoading, qrCodeSrc } = this.state; if (props.step === 1 && !isLoading && !qrCodeSrc) { this.setState({ isLoading: true }); getSecret(this.context.userId).then((resp) => { this.setState({ isLoading: false, secret: resp.secret, qrCodeSrc: resp.qr, }); }); } } nextStep() { const nextStep = this.state.activeStep + 1; if (nextStep < STEPS_TOTAL) { this.props.onChangeStep(nextStep); } } onTotpSubmit = (form: FormModel): Promise => { this.setState({ isLoading: true }); return this.props .onSubmit(form, () => { const data = form.serialize(); return enableMFA(this.context.userId, data.totp, data.password); }) .then(() => this.props.onComplete()) .catch((resp) => { const { errors } = resp || {}; if (errors) { return Promise.reject(errors); } logger.error('MFA: Unexpected form submit result', { resp, }); }) .finally(() => this.setState({ isLoading: false })); }; }