From 39f35192e0c25e38c0c3e93faf1d5f59641313ec Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 13 Feb 2023 16:35:33 +0800 Subject: [PATCH] refactor(ui): use smart input field to replace all password sign-in form (#3065) --- .../src/tests/ui/smoke.test.ts | 4 +- packages/phrases-ui/src/locales/de.ts | 6 +- packages/phrases-ui/src/locales/en.ts | 6 +- packages/phrases-ui/src/locales/fr.ts | 6 +- packages/phrases-ui/src/locales/ko.ts | 6 +- packages/phrases-ui/src/locales/pt-br.ts | 7 +- packages/phrases-ui/src/locales/pt-pt.ts | 6 +- packages/phrases-ui/src/locales/tr-tr.ts | 6 +- packages/phrases-ui/src/locales/zh-cn.ts | 6 +- packages/ui/src/apis/interaction.ts | 9 +- .../components/Input/PasswordInput.test.tsx | 2 +- .../PasswordInputField/index.test.tsx | 2 +- .../SmartInputField/use-smart-input-field.ts | 7 +- .../InputFields/SmartInputField/utils.ts | 8 +- .../containers/CreateAccount/index.test.tsx | 4 +- .../ui/src/containers/CreateAccount/index.tsx | 4 +- .../ui/src/containers/EmailForm/EmailForm.tsx | 4 +- .../EmailPassword/index.module.scss | 36 ---- .../containers/EmailPassword/index.test.tsx | 190 ---------------- .../ui/src/containers/EmailPassword/index.tsx | 103 --------- .../PhonePassword/index.module.scss | 35 --- .../containers/PhonePassword/index.test.tsx | 197 ----------------- .../ui/src/containers/PhonePassword/index.tsx | 123 ----------- .../ui/src/containers/SetPassword/index.tsx | 2 +- .../UsernameForm/UsernameForm.test.tsx | 4 +- .../containers/UsernameForm/UsernameForm.tsx | 4 +- .../containers/UsernameSignIn/index.test.tsx | 203 ----------------- .../src/containers/UsernameSignIn/index.tsx | 100 --------- .../src/pages/SecondarySignIn/index.test.tsx | 18 -- .../ui/src/pages/SecondarySignIn/index.tsx | 7 +- packages/ui/src/pages/SignIn/Main.tsx | 64 +++--- .../PasswordSignInForm}/index.module.scss | 0 .../SignIn/PasswordSignInForm/index.test.tsx | 204 ++++++++++++++++++ .../pages/SignIn/PasswordSignInForm/index.tsx | 146 +++++++++++++ packages/ui/src/pages/SignIn/index.test.tsx | 132 +++--------- packages/ui/src/pages/SignIn/index.tsx | 31 +-- packages/ui/src/utils/country-code.test.ts | 33 +-- packages/ui/src/utils/field-validations.ts | 43 +++- packages/ui/src/utils/form.ts | 49 +++++ 39 files changed, 575 insertions(+), 1242 deletions(-) delete mode 100644 packages/ui/src/containers/EmailPassword/index.module.scss delete mode 100644 packages/ui/src/containers/EmailPassword/index.test.tsx delete mode 100644 packages/ui/src/containers/EmailPassword/index.tsx delete mode 100644 packages/ui/src/containers/PhonePassword/index.module.scss delete mode 100644 packages/ui/src/containers/PhonePassword/index.test.tsx delete mode 100644 packages/ui/src/containers/PhonePassword/index.tsx delete mode 100644 packages/ui/src/containers/UsernameSignIn/index.test.tsx delete mode 100644 packages/ui/src/containers/UsernameSignIn/index.tsx rename packages/ui/src/{containers/UsernameSignIn => pages/SignIn/PasswordSignInForm}/index.module.scss (100%) create mode 100644 packages/ui/src/pages/SignIn/PasswordSignInForm/index.test.tsx create mode 100644 packages/ui/src/pages/SignIn/PasswordSignInForm/index.tsx diff --git a/packages/integration-tests/src/tests/ui/smoke.test.ts b/packages/integration-tests/src/tests/ui/smoke.test.ts index bf9c302f2..a59bc9fed 100644 --- a/packages/integration-tests/src/tests/ui/smoke.test.ts +++ b/packages/integration-tests/src/tests/ui/smoke.test.ts @@ -66,9 +66,9 @@ describe('smoke testing', () => { }); it('signs in to admin console', async () => { - const usernameField = await page.waitForSelector('input[name=username]'); + const usernameField = await page.waitForSelector('input[name=identifier]'); const passwordField = await page.waitForSelector('input[name=password]'); - const submitButton = await page.waitForSelector('button'); + const submitButton = await page.waitForSelector('button[name=submit]'); await usernameField.type(consoleUsername); await passwordField.type(consolePassword); diff --git a/packages/phrases-ui/src/locales/de.ts b/packages/phrases-ui/src/locales/de.ts index cd7e51038..d0b077503 100644 --- a/packages/phrases-ui/src/locales/de.ts +++ b/packages/phrases-ui/src/locales/de.ts @@ -43,6 +43,7 @@ const translation = { description: { email: 'Email', phone_number: 'Telefonnummer', + username: 'benutzername', reminder: 'Erinnerung', not_found: '404 Nicht gefunden', agree_with_terms: 'Ich akzeptiere die ', @@ -98,12 +99,13 @@ const translation = { 'Für zusätzliche Sicherheit, vervollständige bitte deine Informationen.', }, error: { - username_password_mismatch: 'Benutzername oder Passwort ist falsch', + general_required: `{{types, list(type: disjunction;)}} ist erforderlich`, + general_invalid: `Die {{types, list(type: disjunction;)}} is ungültig`, username_required: 'Benutzername ist erforderlich', password_required: 'Passwort ist erforderlich', username_exists: 'Benutzername existiert bereits', username_should_not_start_with_number: 'Benutzername darf nicht mit einer Zahl beginnen', - username_valid_charset: 'Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten', + username_invalid_charset: 'Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten', invalid_email: 'Die Email ist ungültig', invalid_phone: 'Die Telefonnummer ist ungültig', password_min_length: 'Passwort muss mindestens {{min}} Zeichen lang sein', diff --git a/packages/phrases-ui/src/locales/en.ts b/packages/phrases-ui/src/locales/en.ts index 612b5baf8..3f6a25028 100644 --- a/packages/phrases-ui/src/locales/en.ts +++ b/packages/phrases-ui/src/locales/en.ts @@ -41,6 +41,7 @@ const translation = { description: { email: 'email', phone_number: 'phone number', + username: 'username', reminder: 'Reminder', not_found: '404 Not Found', agree_with_terms: 'I have read and agree to the ', @@ -93,12 +94,13 @@ const translation = { continue_with_more_information: 'For added security, please complete below account details.', }, error: { - username_password_mismatch: 'Username and password do not match', + general_required: `{{types, list(type: disjunction;)}} is required`, + general_invalid: `The {{types, list(type: disjunction;)}} is invalid`, username_required: 'Username is required', password_required: 'Password is required', username_exists: 'Username already exists', username_should_not_start_with_number: 'Username should not start with a number', - username_valid_charset: 'Username should only contain letters, numbers, or underscores.', + username_invalid_charset: 'Username should only contain letters, numbers, or underscores.', invalid_email: 'The email is invalid', invalid_phone: 'The phone number is invalid', password_min_length: 'Password requires a minimum of {{min}} characters', diff --git a/packages/phrases-ui/src/locales/fr.ts b/packages/phrases-ui/src/locales/fr.ts index 01ba292ed..6ab81d6e4 100644 --- a/packages/phrases-ui/src/locales/fr.ts +++ b/packages/phrases-ui/src/locales/fr.ts @@ -43,6 +43,7 @@ const translation = { description: { email: 'email', phone_number: 'numéro de téléphone', + username: "nom d'utilisateur", reminder: 'Rappel', not_found: '404 Non trouvé', agree_with_terms: "J'ai lu et accepté les ", @@ -96,13 +97,14 @@ const translation = { continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED }, error: { - username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas", + general_required: `Le {{types, list(type: disjunction;)}} est requis`, + general_invalid: `Le {{types, list(type: disjunction;)}} n'est pas valide`, username_required: "Le nom d'utilisateur est requis", password_required: 'Le mot de passe est requis', username_exists: "Ce Nom d'utilisateur existe déjà", username_should_not_start_with_number: "Le nom d'utilisateur ne doit pas commencer par un chiffre", - username_valid_charset: + username_invalid_charset: "Le nom d'utilisateur ne doit contenir que des lettres, des chiffres ou des caractères de soulignement.", invalid_email: "L'email n'est pas valide", invalid_phone: "Le numéro de téléphone n'est pas valide", diff --git a/packages/phrases-ui/src/locales/ko.ts b/packages/phrases-ui/src/locales/ko.ts index ff9f39909..05b29a89c 100644 --- a/packages/phrases-ui/src/locales/ko.ts +++ b/packages/phrases-ui/src/locales/ko.ts @@ -43,6 +43,7 @@ const translation = { description: { email: '이메일', phone_number: '휴대전화번호', + username: '사용자 이름', reminder: '리마인더', not_found: '404 찾을 수 없음', agree_with_terms: '나는 내용을 읽었으며, 이에 동의합니다.', @@ -92,12 +93,13 @@ const translation = { continue_with_more_information: '더 나은 보안을 위해 아래 자세한 내용을 따라 주세요.', }, error: { - username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.', + general_required: `{{types, list(type: disjunction;)}} 필수예요.`, + general_invalid: `{{types, list(type: disjunction;)}} 유효하지 않아요.`, username_required: '사용자 이름은 필수예요.', password_required: '비밀번호는 필수예요.', username_exists: '사용자 이름이 이미 존재해요.', username_should_not_start_with_number: '사용자 이름은 숫자로 시작하면 안 돼요.', - username_valid_charset: '사용자 이름은 문자, 숫자, _(밑줄 문자) 로만 이루어져야 해요.', + username_invalid_charset: '사용자 이름은 문자, 숫자, _(밑줄 문자) 로만 이루어져야 해요.', invalid_email: '이메일이 유효하지 않아요.', invalid_phone: '휴대전화번호가 유효하지 않아요.', password_min_length: '비밀번호는 최소 {{min}} 자리로 이루어져야 해요.', diff --git a/packages/phrases-ui/src/locales/pt-br.ts b/packages/phrases-ui/src/locales/pt-br.ts index b0d2e9271..f892758d4 100644 --- a/packages/phrases-ui/src/locales/pt-br.ts +++ b/packages/phrases-ui/src/locales/pt-br.ts @@ -43,6 +43,7 @@ const translation = { description: { email: 'e-mail', phone_number: 'número de telefone', + username: 'nome de usuário', reminder: 'Lembrete', not_found: '404 Não Encontrado', agree_with_terms: 'Eu li e concordo com os ', @@ -95,12 +96,14 @@ const translation = { continue_with_more_information: 'Para maior segurança, preencha os detalhes da conta abaixo.', }, error: { - username_password_mismatch: 'Usuário e senha não correspondem', + general_required: `{{types, list(type: disjunction;)}} é obrigatório`, + general_invalid: `O {{types, list(type: disjunction;)}} é inválido`, username_required: 'Nome de usuário é obrigatório', password_required: 'Senha é obrigatório', username_exists: 'O nome de usuário já existe', username_should_not_start_with_number: 'O nome de usuário não deve começar com um número', - username_valid_charset: 'O nome de usuário deve conter apenas letras, números ou sublinhados.', + username_invalid_charset: + 'O nome de usuário deve conter apenas letras, números ou sublinhados.', invalid_email: 'O e-mail é inválido', invalid_phone: 'O número de telefone é inválido', password_min_length: 'A senha requer um mínimo de {{min}} caracteres', diff --git a/packages/phrases-ui/src/locales/pt-pt.ts b/packages/phrases-ui/src/locales/pt-pt.ts index fdbc4e8a9..f99e8fe91 100644 --- a/packages/phrases-ui/src/locales/pt-pt.ts +++ b/packages/phrases-ui/src/locales/pt-pt.ts @@ -43,6 +43,7 @@ const translation = { description: { email: 'email', phone_number: 'telefone', + username: 'utilizador', reminder: 'Lembrete', not_found: '404 Não encontrado', agree_with_terms: 'Eu li e concordo com os ', @@ -93,12 +94,13 @@ const translation = { continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED }, error: { - username_password_mismatch: 'O Utilizador e a password não correspondem', + general_required: `{{types, list(type: disjunction;)}} is necessário`, + general_invalid: `O {{types, list(type: disjunction;)}} é inválido`, username_required: 'Utilizador necessário', password_required: 'Password necessária', username_exists: 'O nome de utilizador já existe', username_should_not_start_with_number: 'O nome de utilizador não deve começar com um número', - username_valid_charset: + username_invalid_charset: 'O nome de utilizador deve conter apenas letras, números ou underscores.', invalid_email: 'O email é inválido', invalid_phone: 'O número de telefone é inválido', diff --git a/packages/phrases-ui/src/locales/tr-tr.ts b/packages/phrases-ui/src/locales/tr-tr.ts index ccbfa4964..6c313b584 100644 --- a/packages/phrases-ui/src/locales/tr-tr.ts +++ b/packages/phrases-ui/src/locales/tr-tr.ts @@ -43,6 +43,7 @@ const translation = { description: { email: 'e-posta adresi', phone_number: 'telefon numarası', + username: 'kullanıcı Adı', reminder: 'Hatırlatıcı', not_found: '404 Bulunamadı', agree_with_terms: 'Okudum ve anladım', @@ -94,12 +95,13 @@ const translation = { continue_with_more_information: 'For added security, please complete below account details.', // UNTRANSLATED }, error: { - username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.', + general_required: `{{types, list(type: disjunction;)}} is required`, // UNTRANSLATED + general_invalid: `The {{types, list(type: disjunction;)}} is invalid`, // UNTRANSLATED username_required: 'Kullanıcı adı gerekli.', password_required: 'Şifre gerekli.', username_exists: 'Kullanıcı adı mevcut.', username_should_not_start_with_number: 'Kullanıcı adı sayı ile başlayamaz.', - username_valid_charset: 'Kullanıcı adı yalnızca harf,sayı veya alt çizgi içermeli.', + username_invalid_charset: 'Kullanıcı adı yalnızca harf,sayı veya alt çizgi içermeli.', invalid_email: 'E-posta adresi geçersiz', invalid_phone: 'Telefon numarası geçersiz', password_min_length: 'Şifre en az {{min}} karakterden oluşmalıdır', diff --git a/packages/phrases-ui/src/locales/zh-cn.ts b/packages/phrases-ui/src/locales/zh-cn.ts index 78d13e86e..590537039 100644 --- a/packages/phrases-ui/src/locales/zh-cn.ts +++ b/packages/phrases-ui/src/locales/zh-cn.ts @@ -43,6 +43,7 @@ const translation = { description: { email: '邮箱', phone_number: '手机', + username: '用户名', reminder: '提示', not_found: '404 页面不存在', agree_with_terms: '我已阅读并同意 ', @@ -89,12 +90,13 @@ const translation = { continue_with_more_information: '为保障您的账号安全,需要您补充以下信息。', }, error: { - username_password_mismatch: '用户名和密码不匹配', + general_required: `{{types, list(type: disjunction;)}}必填`, + general_invalid: `无效的{{types, list(type: disjunction;)}}`, username_required: '用户名必填', password_required: '密码必填', username_exists: '用户名已存在', username_should_not_start_with_number: '用户名不能以数字开头', - username_valid_charset: '用户名只能包含英文字母、数字或下划线。', + username_invalid_charset: '用户名只能包含英文字母、数字或下划线。', invalid_email: '无效的邮箱', invalid_phone: '无效的手机号', password_min_length: '密码最少需要{{min}}个字符', diff --git a/packages/ui/src/apis/interaction.ts b/packages/ui/src/apis/interaction.ts index 5194d0a58..a3a3e0072 100644 --- a/packages/ui/src/apis/interaction.ts +++ b/packages/ui/src/apis/interaction.ts @@ -2,9 +2,7 @@ import { InteractionEvent } from '@logto/schemas'; import type { - UsernamePasswordPayload, - EmailPasswordPayload, - PhonePasswordPayload, + SignInIdentifier, EmailVerificationCodePayload, PhoneVerificationCodePayload, SocialConnectorPayload, @@ -22,10 +20,7 @@ type Response = { redirectTo: string; }; -export type PasswordSignInPayload = - | UsernamePasswordPayload - | EmailPasswordPayload - | PhonePasswordPayload; +export type PasswordSignInPayload = { [K in SignInIdentifier]?: string } & { password: string }; export const signInWithPasswordIdentifier = async (payload: PasswordSignInPayload) => { await api.put(`${interactionPrefix}`, { diff --git a/packages/ui/src/components/Input/PasswordInput.test.tsx b/packages/ui/src/components/Input/PasswordInput.test.tsx index e0642661b..7a68b8a7d 100644 --- a/packages/ui/src/components/Input/PasswordInput.test.tsx +++ b/packages/ui/src/components/Input/PasswordInput.test.tsx @@ -30,7 +30,7 @@ describe('Input Field UI Component', () => { }); test('render error message', () => { - const errorCode = 'username_password_mismatch'; + const errorCode = 'password_required'; const { queryByText } = render(); expect(queryByText(errorCode)).not.toBeNull(); }); diff --git a/packages/ui/src/components/InputFields/PasswordInputField/index.test.tsx b/packages/ui/src/components/InputFields/PasswordInputField/index.test.tsx index 166293f03..25cc0eb62 100644 --- a/packages/ui/src/components/InputFields/PasswordInputField/index.test.tsx +++ b/packages/ui/src/components/InputFields/PasswordInputField/index.test.tsx @@ -30,7 +30,7 @@ describe('Input Field UI Component', () => { }); test('render error message', () => { - const errorCode = 'username_password_mismatch'; + const errorCode = 'password_required'; const { queryByText } = render(); expect(queryByText(errorCode)).not.toBeNull(); }); diff --git a/packages/ui/src/components/InputFields/SmartInputField/use-smart-input-field.ts b/packages/ui/src/components/InputFields/SmartInputField/use-smart-input-field.ts index c81286107..32100dfbf 100644 --- a/packages/ui/src/components/InputFields/SmartInputField/use-smart-input-field.ts +++ b/packages/ui/src/components/InputFields/SmartInputField/use-smart-input-field.ts @@ -26,7 +26,12 @@ const useSmartInputField = ({ onChange, currentType, enabledTypes, onTypeChange const [inputValue, setInputValue] = useState(''); const enabledTypeSet = useMemo(() => new Set(enabledTypes), [enabledTypes]); - assert(enabledTypeSet.has(currentType), new Error('Invalid input type')); + assert( + enabledTypeSet.has(currentType), + new Error( + `Invalid input type. Current inputType ${currentType} is detected but missing in enabledTypes` + ) + ); const detectInputType = useCallback( (value: string) => { diff --git a/packages/ui/src/components/InputFields/SmartInputField/utils.ts b/packages/ui/src/components/InputFields/SmartInputField/utils.ts index d067553e5..5c283bd1b 100644 --- a/packages/ui/src/components/InputFields/SmartInputField/utils.ts +++ b/packages/ui/src/components/InputFields/SmartInputField/utils.ts @@ -3,13 +3,9 @@ import i18next from 'i18next'; import type { HTMLProps } from 'react'; import type { TFuncKey } from 'react-i18next'; -import type { IdentifierInputType, EnabledIdentifierTypes } from './use-smart-input-field'; +import { identifierInputPlaceholderMap } from '@/utils/form'; -const identifierInputPlaceholderMap: { [K in IdentifierInputType]: TFuncKey } = { - [SignInIdentifier.Phone]: 'input.phone_number', - [SignInIdentifier.Email]: 'input.email', - [SignInIdentifier.Username]: 'input.username', -}; +import type { IdentifierInputType, EnabledIdentifierTypes } from './use-smart-input-field'; export const getInputHtmlProps = ( currentType: IdentifierInputType, diff --git a/packages/ui/src/containers/CreateAccount/index.test.tsx b/packages/ui/src/containers/CreateAccount/index.test.tsx index d561e2762..33ecbf3d5 100644 --- a/packages/ui/src/containers/CreateAccount/index.test.tsx +++ b/packages/ui/src/containers/CreateAccount/index.test.tsx @@ -74,7 +74,7 @@ describe('', () => { fireEvent.click(submitButton); - expect(queryByText('username_valid_charset')).not.toBeNull(); + expect(queryByText('username_invalid_charset')).not.toBeNull(); expect(registerWithUsernamePassword).not.toBeCalled(); @@ -83,7 +83,7 @@ describe('', () => { fireEvent.change(usernameInput, { target: { value: 'username' } }); } - expect(queryByText('username_valid_charset')).toBeNull(); + expect(queryByText('username_invalid_charset')).toBeNull(); }); test('password less than 6 chars should throw', () => { diff --git a/packages/ui/src/containers/CreateAccount/index.tsx b/packages/ui/src/containers/CreateAccount/index.tsx index aa2066385..d1472f3a8 100644 --- a/packages/ui/src/containers/CreateAccount/index.tsx +++ b/packages/ui/src/containers/CreateAccount/index.tsx @@ -11,7 +11,7 @@ import useApi from '@/hooks/use-api'; import useForm from '@/hooks/use-form'; import useTerms from '@/hooks/use-terms'; import { - usernameValidation, + validateUsername, passwordValidation, confirmPasswordValidation, } from '@/utils/field-validations'; @@ -94,7 +94,7 @@ const CreateAccount = ({ className, autoFocus }: Props) => { className={styles.inputField} name="new-username" placeholder={t('input.username')} - {...fieldRegister('username', usernameValidation)} + {...fieldRegister('username', validateUsername)} onClear={() => { setFieldValue((state) => ({ ...state, username: '' })); }} diff --git a/packages/ui/src/containers/EmailForm/EmailForm.tsx b/packages/ui/src/containers/EmailForm/EmailForm.tsx index cf746c91b..50ce9f3ff 100644 --- a/packages/ui/src/containers/EmailForm/EmailForm.tsx +++ b/packages/ui/src/containers/EmailForm/EmailForm.tsx @@ -10,7 +10,7 @@ import PasswordlessSwitch from '@/containers/PasswordlessSwitch'; import TermsOfUse from '@/containers/TermsOfUse'; import useForm from '@/hooks/use-form'; import useTerms from '@/hooks/use-terms'; -import { emailValidation } from '@/utils/field-validations'; +import { validateEmail } from '@/utils/field-validations'; import * as styles from './index.module.scss'; @@ -64,7 +64,7 @@ const EmailForm = ({ [validateForm, hasTerms, termsValidation, onSubmit, fieldValue] ); - const { onChange, ...rest } = register('email', emailValidation); + const { onChange, ...rest } = register('email', validateEmail); return (
diff --git a/packages/ui/src/containers/EmailPassword/index.module.scss b/packages/ui/src/containers/EmailPassword/index.module.scss deleted file mode 100644 index d2a57a262..000000000 --- a/packages/ui/src/containers/EmailPassword/index.module.scss +++ /dev/null @@ -1,36 +0,0 @@ -@use '@/scss/underscore' as _; - -.form { - @include _.flex-column; - - > * { - width: 100%; - } - - .inputField, - .link, - .terms, - .formErrors { - margin-bottom: _.unit(4); - } - - .link { - margin-top: _.unit(-1); - width: auto; - align-self: start; - } - - .formErrors { - margin-top: _.unit(-3); - margin-left: _.unit(0.5); - } -} - - -:global(.desktop) { - .form { - .link { - margin-top: _.unit(-2); - } - } -} diff --git a/packages/ui/src/containers/EmailPassword/index.test.tsx b/packages/ui/src/containers/EmailPassword/index.test.tsx deleted file mode 100644 index 517818bda..000000000 --- a/packages/ui/src/containers/EmailPassword/index.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { fireEvent, waitFor } from '@testing-library/react'; -import { act } from 'react-dom/test-utils'; -import { MemoryRouter } from 'react-router-dom'; - -import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; -import { signInWithPasswordIdentifier } from '@/apis/interaction'; -import ConfirmModalProvider from '@/containers/ConfirmModalProvider'; - -import EmailPassword from '.'; - -jest.mock('@/apis/interaction', () => ({ - signInWithPasswordIdentifier: jest.fn(async () => ({ redirectTo: '/' })), -})); -jest.mock('react-device-detect', () => ({ - isMobile: true, -})); -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: jest.fn(), -})); - -describe('', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - }); - - test('render', () => { - const { queryByText, container } = renderWithPageContext(); - expect(container.querySelector('input[name="email"]')).not.toBeNull(); - expect(container.querySelector('input[name="password"]')).not.toBeNull(); - expect(queryByText('action.sign_in')).not.toBeNull(); - }); - - test('render with terms settings enabled', () => { - const { queryByText } = renderWithPageContext( - - - - - - ); - expect(queryByText('description.agree_with_terms')).not.toBeNull(); - expect(queryByText('action.forgot_password')).not.toBeNull(); - }); - - test('required inputs with error message', () => { - const { queryByText, getByText, container } = renderWithPageContext(); - const submitButton = getByText('action.sign_in'); - - fireEvent.click(submitButton); - - expect(queryByText('invalid_email')).not.toBeNull(); - expect(queryByText('password_required')).not.toBeNull(); - - const emailInput = container.querySelector('input[name="email"]'); - const passwordInput = container.querySelector('input[name="password"]'); - - expect(emailInput).not.toBeNull(); - expect(passwordInput).not.toBeNull(); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'email@logto.io' } }); - } - - if (passwordInput) { - fireEvent.change(passwordInput, { target: { value: 'password' } }); - } - - expect(queryByText('invalid_email')).toBeNull(); - expect(queryByText('password_required')).toBeNull(); - }); - - test('should show terms confirm modal', async () => { - const { queryByText, getByText, container } = renderWithPageContext( - - - - - - - - ); - const submitButton = getByText('action.sign_in'); - - const emailInput = container.querySelector('input[name="email"]'); - const passwordInput = container.querySelector('input[name="password"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'email@logto.io' } }); - } - - if (passwordInput) { - fireEvent.change(passwordInput, { target: { value: 'password' } }); - } - - act(() => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(queryByText('description.agree_with_terms_modal')).not.toBeNull(); - }); - }); - - test('should show terms detail modal', async () => { - const { getByText, queryByText, container, queryByRole } = renderWithPageContext( - - - - - - - - ); - const submitButton = getByText('action.sign_in'); - - const emailInput = container.querySelector('input[name="email"]'); - const passwordInput = container.querySelector('input[name="password"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'email@logto.io' } }); - } - - if (passwordInput) { - fireEvent.change(passwordInput, { target: { value: 'password' } }); - } - - act(() => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(queryByText('description.agree_with_terms_modal')).not.toBeNull(); - }); - - const termsLink = getByText('description.terms_of_use'); - - act(() => { - fireEvent.click(termsLink); - }); - - await waitFor(() => { - expect(queryByText('action.agree')).not.toBeNull(); - expect(queryByRole('article')).not.toBeNull(); - }); - }); - - test('submit form', async () => { - const { getByText, container } = renderWithPageContext( - - - - - - ); - const submitButton = getByText('action.sign_in'); - - const emailInput = container.querySelector('input[name="email"]'); - const passwordInput = container.querySelector('input[name="password"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'email' } }); - } - - if (passwordInput) { - fireEvent.change(passwordInput, { target: { value: 'password' } }); - } - - const termsButton = getByText('description.agree_with_terms'); - - act(() => { - fireEvent.click(termsButton); - }); - - act(() => { - fireEvent.click(submitButton); - }); - - act(() => { - void waitFor(() => { - expect(signInWithPasswordIdentifier).toBeCalledWith({ - email: 'email', - password: 'password', - }); - }); - }); - }); -}); diff --git a/packages/ui/src/containers/EmailPassword/index.tsx b/packages/ui/src/containers/EmailPassword/index.tsx deleted file mode 100644 index d83983545..000000000 --- a/packages/ui/src/containers/EmailPassword/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { SignInIdentifier } from '@logto/schemas'; -import classNames from 'classnames'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; - -import Button from '@/components/Button'; -import ErrorMessage from '@/components/ErrorMessage'; -import ForgotPasswordLink from '@/components/ForgotPasswordLink'; -import Input, { PasswordInput } from '@/components/Input'; -import TermsOfUse from '@/containers/TermsOfUse'; -import useForm from '@/hooks/use-form'; -import usePasswordSignIn from '@/hooks/use-password-sign-in'; -import { useForgotPasswordSettings } from '@/hooks/use-sie'; -import useTerms from '@/hooks/use-terms'; -import { emailValidation, requiredValidation } from '@/utils/field-validations'; - -import * as styles from './index.module.scss'; - -type Props = { - className?: string; - // eslint-disable-next-line react/boolean-prop-naming - autoFocus?: boolean; -}; - -type FieldState = { - email: string; - password: string; -}; - -const defaultState: FieldState = { - email: '', - password: '', -}; - -const EmailPassword = ({ className, autoFocus }: Props) => { - const { t } = useTranslation(); - const { termsValidation } = useTerms(); - const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(); - const { isForgotPasswordEnabled, email } = useForgotPasswordSettings(); - - const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState); - - const onSubmitHandler = useCallback( - async (event?: React.FormEvent) => { - event?.preventDefault(); - - clearErrorMessage(); - - if (!validateForm()) { - return; - } - - if (!(await termsValidation())) { - return; - } - - void onSubmit(fieldValue); - }, - [clearErrorMessage, validateForm, termsValidation, onSubmit, fieldValue] - ); - - return ( - - { - setFieldValue((state) => ({ ...state, email: '' })); - }} - /> - requiredValidation('password', value))} - /> - - {errorMessage && {errorMessage}} - - {isForgotPasswordEnabled && ( - - )} - - - -