diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 1afd750a52..bd814fee6e 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -973,6 +973,7 @@ "let_us_know_how_we_can_help": "", "let_us_know_what_you_think": "", "lets_get_those_premium_features": "", + "lets_get_you_set_up": "", "library": "", "licenses": "", "limited_document_history": "", @@ -2109,6 +2110,7 @@ "value_must_be_at_least_x": "", "vat": "", "vat_number": "", + "verification_code": "", "verify_email_address_before_enabling_managed_users": "", "verify_your_email_address": "", "view": "", @@ -2250,6 +2252,7 @@ "your_current_plan_gives_you": "", "your_current_plan_supports_up_to_x_licenses": "", "your_current_project_will_revert_to_the_version_from_time": "", + "your_email_is_confirmed": "", "your_feedback_matters_answer_two_quick_questions": "", "your_git_access_info": "", "your_git_access_info_bullet_1": "", diff --git a/services/web/frontend/js/features/settings/components/emails/ciam-six-digits-input.tsx b/services/web/frontend/js/features/settings/components/emails/ciam-six-digits-input.tsx new file mode 100644 index 0000000000..cda9e56b76 --- /dev/null +++ b/services/web/frontend/js/features/settings/components/emails/ciam-six-digits-input.tsx @@ -0,0 +1,49 @@ +import { forwardRef } from 'react' +import { Form, FormControlProps } from 'react-bootstrap' +import classnames from 'classnames' + +interface CIAMSixDigitsInputProps extends FormControlProps { + value: string | undefined +} + +const separator = '\u2007' // figure space + +const CIAMSixDigitsInput = forwardRef< + HTMLInputElement, + CIAMSixDigitsInputProps +>(({ className, onChange, value, ...props }, ref) => { + const group1 = value?.slice(0, 3) || '' + const group2 = value?.slice(3, 6) || '' + const displayValue = group2 ? `${group1}${separator}${group2}` : group1 + return ( +
+ { + const inputValue = v.target.value + const sanitizedValue = inputValue.replaceAll(/\D/g, '').slice(0, 6) + onChange?.({ + ...v, + target: { ...v.target, value: sanitizedValue }, + currentTarget: { ...v.currentTarget, value: sanitizedValue }, + }) + }} + value={displayValue} + className={classnames( + 'form-control-ds ciam-six-digits-input', + className + )} + maxLength={7} + inputMode="numeric" + /> + + - + +
+ ) +}) +CIAMSixDigitsInput.displayName = 'CIAMSixDigitsInput' + +export default CIAMSixDigitsInput diff --git a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx index 8383d98795..ab9a4bfccd 100644 --- a/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx +++ b/services/web/frontend/js/features/settings/components/emails/confirm-email-form.tsx @@ -2,14 +2,25 @@ 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, MouseEventHandler, useState } from 'react' +import { + ChangeEventHandler, + ComponentProps, + FormEvent, + MouseEventHandler, + useEffect, + useState, +} from 'react' 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 OLFormLabel from '@/shared/components/ol/ol-form-label' import OLButton from '@/shared/components/ol/ol-button' import { useLocation } from '@/shared/hooks/use-location' +import DSFormLabel from '@/shared/components/ds/ds-form-label' +import DSButton from '@/shared/components/ds/ds-button' +import CIAMSixDigitsInput from '@/features/settings/components/emails/ciam-six-digits-input' +import OLFormText from '@/shared/components/ol/ol-form-text' +import DSFormText from '@/shared/components/ds/ds-form-text' type Feedback = { type: 'input' | 'alert' @@ -19,7 +30,7 @@ type Feedback = { type ConfirmEmailFormProps = { confirmationEndpoint: string - flow: string + flow: 'registration' | 'resend' | 'secondary' resendEndpoint: string successMessage?: React.ReactNode successButtonText?: string @@ -32,6 +43,15 @@ type ConfirmEmailFormProps = { isCiam?: boolean } +const OLSixDigitsInput = (props: ComponentProps<'input'>) => ( + +) + export function ConfirmEmailForm({ confirmationEndpoint, flow, @@ -146,7 +166,7 @@ export function ConfirmEmailForm({ }) } - const changeHandler = (e: FormEvent) => { + const changeHandler: ChangeEventHandler = e => { setConfirmationCode(e.currentTarget.value) setFeedback(null) } @@ -161,10 +181,21 @@ export function ConfirmEmailForm({ successMessage={successMessage} successButtonText={successButtonText} redirectTo={successRedirectPath} + autoRedirect={isCiam ? 8000 : false} /> ) } + const longLabel = isModal + ? t('enter_the_code', { email }) + : t('enter_the_confirmation_code', { email }) + + const Button = isCiam ? DSButton : OLButton + const buttonSize = isCiam ? 'lg' : undefined + + const SixDigits = isCiam ? CIAMSixDigitsInput : OLSixDigitsInput + const FormText = isCiam ? DSFormText : OLFormText + return (
- - {isModal - ? t('enter_the_code', { email }) - : t('enter_the_confirmation_code', { email })} - - {longLabel}

} + + {isCiam ? ( + + {t('verification_code')} + + ) : ( + {longLabel} + )} + +
{feedback?.type === 'input' && ( -
- -
- -
-
+ + + )}
- {t('confirm')} - - + {onCancel && ( )}
+ {isCiam && flow === 'registration' && ( +
+ + sendMB('email-verification-click', { + button: 'change-email', + flow, + }) + } + />, + ]} + /> +
+ )} ) @@ -281,10 +332,12 @@ function ConfirmEmailSuccessfullForm({ successMessage, successButtonText, redirectTo, + autoRedirect = false, }: { successMessage: React.ReactNode successButtonText: string redirectTo: string + autoRedirect?: number | false }) { const location = useLocation() const submitHandler = (e: FormEvent) => { @@ -292,15 +345,24 @@ function ConfirmEmailSuccessfullForm({ location.assign(redirectTo) } + useEffect(() => { + if (autoRedirect) { + const timer = setTimeout(() => location.assign(redirectTo), autoRedirect) + return () => clearTimeout(timer) + } + }, [autoRedirect, location, redirectTo]) + return ( -
+
{successMessage}
-
- - {successButtonText} - -
+ {!autoRedirect && ( +
+ + {successButtonText} + +
+ )}
) } diff --git a/services/web/frontend/js/shared/components/ds/ds-button.tsx b/services/web/frontend/js/shared/components/ds/ds-button.tsx index 9fd8342f60..6a45aabae9 100644 --- a/services/web/frontend/js/shared/components/ds/ds-button.tsx +++ b/services/web/frontend/js/shared/components/ds/ds-button.tsx @@ -1,5 +1,7 @@ import { forwardRef, ReactNode } from 'react' -import { Button, ButtonProps } from 'react-bootstrap' +import { Button, ButtonProps, Spinner } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import classNames from 'classnames' type DSButtonProps = Pick< ButtonProps, @@ -22,6 +24,8 @@ type DSButtonProps = Pick< leadingIcon?: ReactNode trailingIcon?: ReactNode variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' + isLoading?: boolean + loadingLabel?: string } const DSButton = forwardRef( @@ -29,6 +33,8 @@ const DSButton = forwardRef( { children, leadingIcon, + isLoading = false, + loadingLabel, trailingIcon, variant = 'primary', size, @@ -36,16 +42,40 @@ const DSButton = forwardRef( }, ref ) => { + const { t } = useTranslation() + + const buttonClassName = classNames('d-inline-grid btn-ds', { + 'button-loading': isLoading, + }) + + const loadingSpinnerClassName = + size === 'lg' ? 'loading-spinner-large' : 'loading-spinner-small' + return (