diff --git a/packages/onboarding-ui/.i18n/en.i18n.json b/packages/onboarding-ui/.i18n/en.i18n.json index 4d2e6cfa63..fcdbbd7b93 100644 --- a/packages/onboarding-ui/.i18n/en.i18n.json +++ b/packages/onboarding-ui/.i18n/en.i18n.json @@ -309,6 +309,27 @@ "button": { "text": "Save new password" } + }, + "totpForm": { + "fields": { + "totpCode": { + "label": "TOTP Code", + "placeholder": "TOTP Code" + }, + "backupCode": { + "label": "Backup Code", + "placeholder": "Backup Code" + } + }, + "button": { + "text": "Log in" + }, + "buttonBackupCode": { + "text": "Need to use backup code?" + }, + "buttonTotpCode": { + "text": "Use TOTP code." + } } } } diff --git a/packages/onboarding-ui/src/forms/TotpForm/Totp.stories.tsx b/packages/onboarding-ui/src/forms/TotpForm/Totp.stories.tsx new file mode 100644 index 0000000000..03cfc8460d --- /dev/null +++ b/packages/onboarding-ui/src/forms/TotpForm/Totp.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, Story } from '@storybook/react'; +import type { ComponentProps } from 'react'; + +import TotpForm from './TotpForm'; + +type Args = ComponentProps; + +export default { + title: 'forms/TotpForm', + component: TotpForm, + parameters: { + layout: 'centered', + actions: { argTypesRegex: '^on.*' }, + }, +} as Meta; + +export const _TotpForm: Story = (args) => ; +_TotpForm.storyName = 'TotpForm'; diff --git a/packages/onboarding-ui/src/forms/TotpForm/TotpForm.spec.tsx b/packages/onboarding-ui/src/forms/TotpForm/TotpForm.spec.tsx new file mode 100644 index 0000000000..5db6f567fb --- /dev/null +++ b/packages/onboarding-ui/src/forms/TotpForm/TotpForm.spec.tsx @@ -0,0 +1,16 @@ +import ReactDOM from 'react-dom'; + +import TotpForm from './TotpForm'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render( + undefined} + isBackupCode={false} + onSubmit={() => undefined} + />, + div + ); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/packages/onboarding-ui/src/forms/TotpForm/TotpForm.styles.tsx b/packages/onboarding-ui/src/forms/TotpForm/TotpForm.styles.tsx new file mode 100644 index 0000000000..a0da98fdca --- /dev/null +++ b/packages/onboarding-ui/src/forms/TotpForm/TotpForm.styles.tsx @@ -0,0 +1,27 @@ +import styled from '@rocket.chat/styled'; + +export const TotpActionsWrapper = styled('div')` + width: 100%; + box-sizing: border-box; + display: flex; + flex-flow: column nowrap; + align-items: flex-start; + justify-content: stretch; + + a { + margin-block-start: 16px; + } + + @media (min-width: 1440px) { + flex-flow: row nowrap; + padding: 0; + width: 100%; + align-items: center; + max-width: 1152px; + + a { + padding-inline: 8px; + margin-block-start: 0; + } + } +`; diff --git a/packages/onboarding-ui/src/forms/TotpForm/TotpForm.tsx b/packages/onboarding-ui/src/forms/TotpForm/TotpForm.tsx new file mode 100644 index 0000000000..24cc893444 --- /dev/null +++ b/packages/onboarding-ui/src/forms/TotpForm/TotpForm.tsx @@ -0,0 +1,104 @@ +import { + FieldGroup, + Field, + NumberInput, + TextInput, + Button, +} from '@rocket.chat/fuselage'; +import type { ReactElement } from 'react'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +import ActionLink from '../../common/ActionLink'; +import Form from '../../common/Form'; +import { TotpActionsWrapper } from './TotpForm.styles'; + +export type TotpFormPayload = { + totpCode: string; + backupCode: string; +}; + +type TotpFormProps = { + initialValues?: TotpFormPayload; + onChangeTotpForm: () => void; + isBackupCode?: boolean; + formError?: string; + onSubmit: SubmitHandler; +}; + +const TotpForm = ({ + onSubmit, + initialValues, + isBackupCode = false, + onChangeTotpForm, +}: TotpFormProps): ReactElement => { + const { t } = useTranslation(); + + const { + register, + handleSubmit, + formState: { errors, isValidating, isSubmitting }, + } = useForm({ + defaultValues: { + ...initialValues, + }, + }); + + return ( +
+ + + + {isBackupCode ? ( + + {t('form.totpForm.fields.backupCode.label')} + + ) : ( + + {t('form.totpForm.fields.totpCode.label')} + + )} + + {isBackupCode ? ( + + ) : ( + + )} + + {errors.backupCode && ( + {errors.backupCode.message} + )} + {errors.totpCode && ( + {errors.totpCode.message} + )} + + + + + + + + {isBackupCode + ? t('form.totpForm.buttonTotpCode.text') + : t('form.totpForm.buttonBackupCode.text')} + + + +
+ ); +}; + +export default TotpForm; diff --git a/packages/onboarding-ui/src/forms/TotpForm/index.ts b/packages/onboarding-ui/src/forms/TotpForm/index.ts new file mode 100644 index 0000000000..a05d945c1d --- /dev/null +++ b/packages/onboarding-ui/src/forms/TotpForm/index.ts @@ -0,0 +1 @@ +export { default } from './TotpForm'; diff --git a/packages/onboarding-ui/src/pages/LoginPage/LoginPage.spec.tsx b/packages/onboarding-ui/src/pages/LoginPage/LoginPage.spec.tsx index 823f1381f8..fca9a2b76b 100644 --- a/packages/onboarding-ui/src/pages/LoginPage/LoginPage.spec.tsx +++ b/packages/onboarding-ui/src/pages/LoginPage/LoginPage.spec.tsx @@ -6,6 +6,7 @@ it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render( undefined} onResetPassword={() => undefined} diff --git a/packages/onboarding-ui/src/pages/LoginPage/LoginPage.tsx b/packages/onboarding-ui/src/pages/LoginPage/LoginPage.tsx index 8227181d3c..f242b169e5 100644 --- a/packages/onboarding-ui/src/pages/LoginPage/LoginPage.tsx +++ b/packages/onboarding-ui/src/pages/LoginPage/LoginPage.tsx @@ -8,6 +8,16 @@ import BackgroundLayer from '../../common/BackgroundLayer'; import { OnboardingLogo } from '../../common/OnboardingLogo'; import LoginForm from '../../forms/LoginForm'; import type { LoginFormPayload } from '../../forms/LoginForm/LoginForm'; +import TotpForm from '../../forms/TotpForm'; +import type { TotpFormPayload } from '../../forms/TotpForm/TotpForm'; + +type TotpFormProps = { + initialValues?: TotpFormPayload; + onChangeTotpForm: () => void; + isBackupCode?: boolean; + formError?: string; + onSubmit: SubmitHandler; +}; type LoginPageProps = { initialValues?: Omit; @@ -15,7 +25,9 @@ type LoginPageProps = { onResetPassword: () => void; formError?: string; isPasswordLess: boolean; + isMfa: boolean; onCreateAccount: () => void; + mfaProps?: TotpFormProps; onSubmit: SubmitHandler; }; @@ -24,6 +36,7 @@ const LoginPage = ({ ...props }: LoginPageProps): ReactElement => { const { t } = useTranslation(); + const { isMfa, mfaProps } = props; return ( @@ -51,20 +64,26 @@ const LoginPage = ({ - + {isMfa && !!mfaProps ? ( + + ) : ( + + )} - - New here? - - Create account - - + {!isMfa && ( + + New here? + + Create account + + + )}