import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import OtpInput from 'react-otp-input'; import { verifyCredentials } from 'soapbox/actions/auth'; import { closeModal } from 'soapbox/actions/modals'; import snackbar from 'soapbox/actions/snackbar'; import { reConfirmPhoneVerification, reRequestPhoneVerification } from 'soapbox/actions/verification'; import { FormGroup, PhoneInput, Modal, Stack, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { getAccessToken } from 'soapbox/utils/auth'; const messages = defineMessages({ verificationInvalid: { id: 'sms_verification.invalid', defaultMessage: 'Please enter a valid phone number.', }, verificationSuccess: { id: 'sms_verification.success', defaultMessage: 'A verification code has been sent to your phone number.', }, verificationFail: { id: 'sms_verification.fail', defaultMessage: 'Failed to send SMS message to your phone number.', }, verificationExpired: { id: 'sms_verification.expired', defaultMessage: 'Your SMS token has expired.', }, verifySms: { id: 'sms_verification.modal.verify_sms', defaultMessage: 'Verify SMS', }, verifyNumber: { id: 'sms_verification.modal.verify_number', defaultMessage: 'Verify phone number', }, verifyCode: { id: 'sms_verification.modal.verify_code', defaultMessage: 'Verify code', }, }); interface IVerifySmsModal { onClose: (type: string) => void, } enum Statuses { IDLE = 'IDLE', READY = 'READY', REQUESTED = 'REQUESTED', FAIL = 'FAIL', SUCCESS = 'SUCCESS', } const VerifySmsModal: React.FC = ({ onClose }) => { const dispatch = useAppDispatch(); const intl = useIntl(); const accessToken = useAppSelector((state) => getAccessToken(state)); const title = useAppSelector((state) => state.instance.title); const isLoading = useAppSelector((state) => state.verification.isLoading); const [status, setStatus] = useState(Statuses.IDLE); const [phone, setPhone] = useState(); const [verificationCode, setVerificationCode] = useState(''); const [requestedAnother, setAlreadyRequestedAnother] = useState(false); const isValid = !!phone; const onChange = useCallback((phone?: string) => { setPhone(phone); }, []); const handleSubmit = (event: React.MouseEvent) => { event.preventDefault(); if (!isValid) { setStatus(Statuses.IDLE); dispatch( snackbar.error( intl.formatMessage(messages.verificationInvalid), ), ); return; } dispatch(reRequestPhoneVerification(phone!)).then(() => { dispatch( snackbar.success( intl.formatMessage(messages.verificationSuccess), ), ); }) .finally(() => setStatus(Statuses.REQUESTED)) .catch(() => { dispatch( snackbar.error( intl.formatMessage(messages.verificationFail), ), ); }); }; const resendVerificationCode = (event?: React.MouseEvent) => { setAlreadyRequestedAnother(true); handleSubmit(event as React.MouseEvent); }; const onConfirmationClick = (event: any) => { switch (status) { case Statuses.IDLE: setStatus(Statuses.READY); break; case Statuses.READY: handleSubmit(event); break; case Statuses.REQUESTED: submitVerification(); break; default: break; } }; const confirmationText = useMemo(() => { switch (status) { case Statuses.IDLE: return intl.formatMessage(messages.verifySms); case Statuses.READY: return intl.formatMessage(messages.verifyNumber); case Statuses.REQUESTED: return intl.formatMessage(messages.verifyCode); default: return null; } }, [status]); const renderModalBody = () => { switch (status) { case Statuses.IDLE: return ( ); case Statuses.READY: return ( ); case Statuses.REQUESTED: return ( <> ); default: return null; } }; const submitVerification = () => { // TODO: handle proper validation from Pepe -- expired vs invalid dispatch(reConfirmPhoneVerification(verificationCode)) .then(() => { setStatus(Statuses.SUCCESS); // eslint-disable-next-line promise/catch-or-return dispatch(verifyCredentials(accessToken)) .then(() => dispatch(closeModal('VERIFY_SMS'))); }) .catch(() => dispatch( snackbar.error( intl.formatMessage(messages.verificationExpired), ), )); }; useEffect(() => { if (verificationCode.length === 6) { submitVerification(); } }, [verificationCode]); return ( } onClose={() => onClose('VERIFY_SMS')} cancelAction={status === Statuses.IDLE ? () => onClose('VERIFY_SMS') : undefined} cancelText='Skip for now' confirmationAction={onConfirmationClick} confirmationText={confirmationText} secondaryAction={status === Statuses.REQUESTED ? resendVerificationCode : undefined} secondaryText={status === Statuses.REQUESTED ? ( ) : undefined} secondaryDisabled={requestedAnother} > {renderModalBody()} ); }; export default VerifySmsModal;