|
|
@@ -0,0 +1,326 @@
|
|
|
+import { postJSON } from '@/infrastructure/fetch-json'
|
|
|
+import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n'
|
|
|
+import Notification from '@/shared/components/notification'
|
|
|
+import getMeta from '@/utils/meta'
|
|
|
+import { FormEvent, useState } from 'react'
|
|
|
+import { Button } from 'react-bootstrap'
|
|
|
+import { Trans, useTranslation } from 'react-i18next'
|
|
|
+import LoadingSpinner from '@/shared/components/loading-spinner'
|
|
|
+import MaterialIcon from '@/shared/components/material-icon'
|
|
|
+import { sendMB } from '@/infrastructure/event-tracking'
|
|
|
+import { Interstitial } from '@/shared/components/interstitial'
|
|
|
+
|
|
|
+type Feedback = {
|
|
|
+ type: 'input' | 'alert'
|
|
|
+ style: 'error' | 'info'
|
|
|
+ message: string
|
|
|
+}
|
|
|
+
|
|
|
+type ConfirmEmailFormProps = {
|
|
|
+ isRegistrationForm: boolean
|
|
|
+}
|
|
|
+
|
|
|
+export function ConfirmEmailForm({
|
|
|
+ isRegistrationForm,
|
|
|
+}: ConfirmEmailFormProps) {
|
|
|
+ const { t } = useTranslation()
|
|
|
+ const [confirmationCode, setConfirmationCode] = useState('')
|
|
|
+ const [feedback, setFeedback] = useState<Feedback | null>(null)
|
|
|
+ const [isConfirming, setIsConfirming] = useState(false)
|
|
|
+ const [isResending, setIsResending] = useState(false)
|
|
|
+ const [successRedirectPath, setSuccessRedirectPath] = useState('')
|
|
|
+ const email = getMeta('ol-email')
|
|
|
+ const { isReady } = useWaitForI18n()
|
|
|
+
|
|
|
+ const errorHandler = (err: any, actionType?: string) => {
|
|
|
+ let errorName = err?.data?.message?.key || 'generic_something_went_wrong'
|
|
|
+
|
|
|
+ if (err?.response?.status === 429) {
|
|
|
+ if (actionType === 'confirm') {
|
|
|
+ errorName = 'too_many_confirm_code_verification_attempts'
|
|
|
+ } else if (actionType === 'resend') {
|
|
|
+ errorName = 'too_many_confirm_code_resend_attempts'
|
|
|
+ }
|
|
|
+ setFeedback({
|
|
|
+ type: 'alert',
|
|
|
+ style: 'error',
|
|
|
+ message: errorName,
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ setFeedback({
|
|
|
+ type: 'input',
|
|
|
+ style: 'error',
|
|
|
+ message: errorName,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ sendMB('email-verification-error', {
|
|
|
+ errorName,
|
|
|
+ flow: isRegistrationForm ? 'registration' : 'secondary',
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const invalidFormHandler = () => {
|
|
|
+ if (!confirmationCode) {
|
|
|
+ return setFeedback({
|
|
|
+ type: 'input',
|
|
|
+ style: 'error',
|
|
|
+ message: 'please_enter_confirmation_code',
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const submitHandler = (e: FormEvent<HTMLFormElement>) => {
|
|
|
+ e.preventDefault()
|
|
|
+ setIsConfirming(true)
|
|
|
+ setFeedback(null)
|
|
|
+
|
|
|
+ const requestPath = isRegistrationForm
|
|
|
+ ? '/registration/confirm-email'
|
|
|
+ : '/user/emails/confirm-secondary'
|
|
|
+
|
|
|
+ const requestBody = isRegistrationForm
|
|
|
+ ? {
|
|
|
+ _csrf: getMeta('ol-csrfToken'),
|
|
|
+ code: confirmationCode,
|
|
|
+ email,
|
|
|
+ }
|
|
|
+ : {
|
|
|
+ code: confirmationCode,
|
|
|
+ }
|
|
|
+
|
|
|
+ postJSON(requestPath, {
|
|
|
+ body: requestBody,
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ setSuccessRedirectPath(data?.redir || '/')
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ errorHandler(err, 'confirm')
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ setIsConfirming(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ sendMB('email-verification-click', {
|
|
|
+ button: 'verify',
|
|
|
+ flow: isRegistrationForm ? 'registration' : 'secondary',
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const resendHandler = (e: FormEvent<Button>) => {
|
|
|
+ setIsResending(true)
|
|
|
+ setFeedback(null)
|
|
|
+
|
|
|
+ const requestPath = isRegistrationForm
|
|
|
+ ? '/registration/confirm-email/resend'
|
|
|
+ : '/user/emails/resend-secondary-confirmation'
|
|
|
+
|
|
|
+ const requestBody = isRegistrationForm
|
|
|
+ ? {
|
|
|
+ _csrf: getMeta('ol-csrfToken'),
|
|
|
+ email,
|
|
|
+ }
|
|
|
+ : {}
|
|
|
+
|
|
|
+ postJSON(requestPath, {
|
|
|
+ body: requestBody,
|
|
|
+ })
|
|
|
+ .then(data => {
|
|
|
+ setIsResending(false)
|
|
|
+ if (data?.message?.key) {
|
|
|
+ setFeedback({
|
|
|
+ type: 'alert',
|
|
|
+ style: 'info',
|
|
|
+ message: data.message.key,
|
|
|
+ })
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .catch(err => {
|
|
|
+ errorHandler(err, 'resend')
|
|
|
+ })
|
|
|
+ .finally(() => {
|
|
|
+ setIsResending(false)
|
|
|
+ })
|
|
|
+
|
|
|
+ sendMB('email-verification-click', {
|
|
|
+ button: 'resend',
|
|
|
+ flow: isRegistrationForm ? 'registration' : 'secondary',
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const changeHandler = (e: FormEvent<HTMLInputElement>) => {
|
|
|
+ setConfirmationCode(e.currentTarget.value)
|
|
|
+ setFeedback(null)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isReady) {
|
|
|
+ return (
|
|
|
+ <Interstitial className="confirm-email" showLogo>
|
|
|
+ <LoadingSpinner />
|
|
|
+ </Interstitial>
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ if (successRedirectPath) {
|
|
|
+ return (
|
|
|
+ <ConfirmEmailSuccessfullForm
|
|
|
+ isRegistrationForm={isRegistrationForm}
|
|
|
+ redirectTo={successRedirectPath}
|
|
|
+ />
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Interstitial className="confirm-email" showLogo>
|
|
|
+ <form onSubmit={submitHandler} onInvalid={invalidFormHandler}>
|
|
|
+ {feedback?.type === 'alert' && (
|
|
|
+ <Notification
|
|
|
+ ariaLive="polite"
|
|
|
+ className="confirm-email-alert"
|
|
|
+ type={feedback.style}
|
|
|
+ content={<ErrorMessage error={feedback.message} />}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+
|
|
|
+ <h1 className="h3 interstitial-header">{t('confirm_your_email')}</h1>
|
|
|
+
|
|
|
+ <p className="small">{t('enter_the_confirmation_code', { email })}</p>
|
|
|
+ <input
|
|
|
+ className="form-control"
|
|
|
+ placeholder={t('enter_6_digit_code')}
|
|
|
+ inputMode="numeric"
|
|
|
+ required
|
|
|
+ value={confirmationCode}
|
|
|
+ onChange={changeHandler}
|
|
|
+ data-ol-dirty={feedback ? 'true' : undefined}
|
|
|
+ maxLength={6}
|
|
|
+ autoComplete="one-time-code"
|
|
|
+ autoFocus // eslint-disable-line jsx-a11y/no-autofocus
|
|
|
+ />
|
|
|
+ <div aria-live="polite">
|
|
|
+ {feedback?.type === 'input' && (
|
|
|
+ <div className="small text-danger">
|
|
|
+ <MaterialIcon className="icon" type="error" />
|
|
|
+ <div>
|
|
|
+ <ErrorMessage error={feedback.message} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-actions">
|
|
|
+ <Button
|
|
|
+ disabled={isConfirming || isResending}
|
|
|
+ type="submit"
|
|
|
+ bsStyle={null}
|
|
|
+ className="btn-primary"
|
|
|
+ >
|
|
|
+ {isConfirming ? (
|
|
|
+ <>
|
|
|
+ {t('confirming')}
|
|
|
+ <span>…</span>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ t('confirm')
|
|
|
+ )}
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ disabled={isConfirming || isResending}
|
|
|
+ onClick={resendHandler}
|
|
|
+ bsStyle={null}
|
|
|
+ className="btn-secondary"
|
|
|
+ >
|
|
|
+ {isResending ? (
|
|
|
+ <>
|
|
|
+ {t('resending_confirmation_code')}
|
|
|
+ <span>…</span>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ t('resend_confirmation_code')
|
|
|
+ )}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </Interstitial>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function ConfirmEmailSuccessfullForm({
|
|
|
+ isRegistrationForm,
|
|
|
+ redirectTo,
|
|
|
+}: {
|
|
|
+ isRegistrationForm: boolean
|
|
|
+ redirectTo: string
|
|
|
+}) {
|
|
|
+ const { t } = useTranslation()
|
|
|
+ const submitHandler = (e: FormEvent<HTMLFormElement>) => {
|
|
|
+ e.preventDefault()
|
|
|
+ location.assign(redirectTo)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Interstitial className="confirm-email" showLogo>
|
|
|
+ <form onSubmit={submitHandler}>
|
|
|
+ <div aria-live="polite">
|
|
|
+ <h1 className="h3 interstitial-header">
|
|
|
+ {isRegistrationForm
|
|
|
+ ? t('email_confirmed_onboarding')
|
|
|
+ : t('thanks_for_confirming_your_email_address')}
|
|
|
+ </h1>
|
|
|
+ <p className="small">
|
|
|
+ {isRegistrationForm && (
|
|
|
+ <Trans
|
|
|
+ i18nKey="email_confirmed_onboarding_message"
|
|
|
+ /* eslint-disable-next-line react/jsx-key */
|
|
|
+ components={[<strong />]}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="form-actions">
|
|
|
+ <Button type="submit" bsStyle={null} className="btn-primary">
|
|
|
+ {isRegistrationForm ? t('continue') : t('go_to_overleaf')}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </Interstitial>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+function ErrorMessage({ error }: { error: string }) {
|
|
|
+ const { t } = useTranslation()
|
|
|
+
|
|
|
+ switch (error) {
|
|
|
+ case 'invalid_confirmation_code':
|
|
|
+ return <span>{t('invalid_confirmation_code')}</span>
|
|
|
+
|
|
|
+ case 'expired_confirmation_code':
|
|
|
+ return (
|
|
|
+ <Trans
|
|
|
+ i18nKey="expired_confirmation_code"
|
|
|
+ /* eslint-disable-next-line react/jsx-key */
|
|
|
+ components={[<strong />]}
|
|
|
+ />
|
|
|
+ )
|
|
|
+
|
|
|
+ case 'email_already_registered':
|
|
|
+ return <span>{t('email_already_registered')}</span>
|
|
|
+
|
|
|
+ case 'too_many_confirm_code_resend_attempts':
|
|
|
+ return <span>{t('too_many_confirm_code_resend_attempts')}</span>
|
|
|
+
|
|
|
+ case 'too_many_confirm_code_verification_attempts':
|
|
|
+ return <span>{t('too_many_confirm_code_verification_attempts')}</span>
|
|
|
+
|
|
|
+ case 'we_sent_new_code':
|
|
|
+ return <span>{t('we_sent_new_code')}</span>
|
|
|
+
|
|
|
+ case 'please_enter_confirmation_code':
|
|
|
+ return <span>{t('please_enter_confirmation_code')}</span>
|
|
|
+
|
|
|
+ default:
|
|
|
+ return <span>{t('generic_something_went_wrong')}</span>
|
|
|
+ }
|
|
|
+}
|