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 (
-
)
}
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 (