// SPDX-License-Identifier: AGPL-3.0-or-later import * as Modal from '@app/features/app/components/dialogs/Modal'; import {EXAMPLE_PERSONAL_EMAIL} from '@app/features/app/config/I18nDisplayConstants'; import {useFormSubmit} from '@app/features/app/hooks/useFormSubmit'; import {VERIFICATION_CODE_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {Button} from '@app/features/ui/button/Button'; import * as ModalCommands from '@app/features/ui/commands/ModalCommands'; import * as ToastCommands from '@app/features/ui/commands/ToastCommands'; import {Form} from '@app/features/ui/components/form/Form'; import {Input} from '@app/features/ui/components/form/FormInput'; import {SteppedCarousel} from '@app/features/ui/stepped_carousel/SteppedCarousel'; import * as UserCommands from '@app/features/user/commands/UserCommands'; import Users from '@app/features/user/state/Users'; import { EMAIL_CHANGED_DESCRIPTOR, INVALID_OR_EXPIRED_CODE_DESCRIPTOR, NEW_EMAIL_DESCRIPTOR, NEW_EMAIL_FORM_DESCRIPTOR, UNABLE_TO_RESEND_CODE_RIGHT_NOW_DESCRIPTOR, } from '@app/features/user/utils/UserMessageDescriptors'; import * as FormUtils from '@app/lib/forms'; import {pushApiErrorModal} from '@app/lib/forms'; import {msg} from '@lingui/core/macro'; import {Trans, useLingui} from '@lingui/react/macro'; import {observer} from 'mobx-react-lite'; import {useEffect, useMemo, useState} from 'react'; import {useForm} from 'react-hook-form'; const ADD_A_WORKING_EMAIL_DESCRIPTOR = msg({ message: 'Add a working email', comment: 'Title of the modal that replaces an email address that bounced. Keep it concise.', }); type Stage = 'newEmail' | 'verifyNew'; const STAGE_ORDER: ReadonlyArray = ['newEmail', 'verifyNew']; const RESEND_COOLDOWN_MS = 30 * 1000; interface NewEmailForm { email: string; } export const BouncedEmailChangeModal = observer(() => { const {i18n} = useLingui(); const newEmailForm = useForm({defaultValues: {email: ''}}); const [stage, setStage] = useState('newEmail'); const [ticket, setTicket] = useState(null); const [code, setCode] = useState(''); const [codeError, setCodeError] = useState(null); const [resendAt, setResendAt] = useState(null); const [submitting, setSubmitting] = useState(false); const [now, setNow] = useState(Date.now()); useEffect(() => { const id = window.setInterval(() => setNow(Date.now()), 1000); return () => window.clearInterval(id); }, []); const canResend = useMemo(() => !resendAt || resendAt.getTime() <= now, [resendAt, now]); const secondsRemaining = useMemo( () => (resendAt ? Math.max(0, Math.ceil((resendAt.getTime() - now) / 1000)) : 0), [resendAt, now], ); const handleRequestNew = async (data: NewEmailForm) => { setSubmitting(true); try { const result = await UserCommands.requestBouncedEmailChangeNew(data.email); setTicket(result.ticket); setResendAt(result.resend_available_at ? new Date(result.resend_available_at) : null); setCode(''); setCodeError(null); setStage('verifyNew'); } finally { setSubmitting(false); } }; const handleResend = async () => { if (!ticket || !canResend) return; setSubmitting(true); try { await UserCommands.resendBouncedEmailChangeNew(ticket); setResendAt(new Date(Date.now() + RESEND_COOLDOWN_MS)); } catch (error: unknown) { pushApiErrorModal(i18n, error, i18n._(UNABLE_TO_RESEND_CODE_RIGHT_NOW_DESCRIPTOR)); } finally { setSubmitting(false); } }; const handleVerify = async () => { if (!ticket) return; setSubmitting(true); setCodeError(null); try { const updatedUser = await UserCommands.verifyBouncedEmailChangeNew(ticket, code.split(' ').join('')); Users.handleUserUpdate(updatedUser, {clearMissingOptionalFields: true}); ToastCommands.createToast({type: 'success', children: i18n._(EMAIL_CHANGED_DESCRIPTOR)}); ModalCommands.pop(); } catch (error: unknown) { setCodeError( error && typeof error === 'object' && 'body' in error ? FormUtils.extractErrorMessage(i18n, error) : i18n._(INVALID_OR_EXPIRED_CODE_DESCRIPTOR), ); } finally { setSubmitting(false); } }; const {handleSubmit: handleNewEmailSubmit} = useFormSubmit({ form: newEmailForm, onSubmit: handleRequestNew, defaultErrorField: 'email', }); const renderNewEmailStage = () => (
Emails to your current address are not being delivered. Enter a new email and we'll send a code there.
); const renderVerifyNewStage = () => ( <> Enter the code we emailed to your new address. ) => setCode(event.target.value)} label={i18n._(VERIFICATION_CODE_DESCRIPTOR)} placeholder="XXXX-XXXX" required={true} error={codeError ?? undefined} data-flx="user.bounced-email-change-modal.input.set-code" /> ); return ( {stage === 'newEmail' ? renderNewEmailStage() : renderVerifyNewStage()} {stage === 'newEmail' ? ( ) : ( <> )} ); });