From 8815b0b048653510714df6db316cb55b38362d4f Mon Sep 17 00:00:00 2001 From: simeng-li Date: Wed, 15 Feb 2023 11:46:40 +0800 Subject: [PATCH] refactor(ui): refactor continue flow (#3108) --- packages/ui/src/App.tsx | 2 - .../EmailForm/EmailContinue.test.tsx | 51 ----- .../containers/EmailForm/EmailContinue.tsx | 52 ----- .../containers/EmailForm/EmailForm.test.tsx | 172 ----------------- .../ui/src/containers/EmailForm/EmailForm.tsx | 99 ---------- .../containers/EmailForm/index.module.scss | 31 --- .../ui/src/containers/EmailForm/index.tsx | 1 - .../PasswordlessSwitch/index.test.tsx | 29 --- .../containers/PasswordlessSwitch/index.tsx | 32 ---- .../PhoneForm/PhoneContinue.test.tsx | 59 ------ .../containers/PhoneForm/PhoneContinue.tsx | 52 ----- .../containers/PhoneForm/PhoneForm.test.tsx | 179 ------------------ .../ui/src/containers/PhoneForm/PhoneForm.tsx | 117 ------------ .../containers/PhoneForm/index.module.scss | 31 --- .../ui/src/containers/PhoneForm/index.tsx | 1 - .../UsernameForm/SetUsername/index.test.tsx | 43 ----- .../UsernameForm/SetUsername/index.tsx | 23 --- .../UsernameForm/UsernameForm.test.tsx | 136 ------------- .../containers/UsernameForm/UsernameForm.tsx | 92 --------- .../ui/src/containers/UsernameForm/index.ts | 1 - packages/ui/src/hooks/use-form.ts | 72 ------- .../use-required-profile-error-handler.ts | 14 +- .../use-send-verification-code-legacy.ts | 79 -------- .../Continue/EmailOrPhone/index.test.tsx | 48 ----- .../src/pages/Continue/EmailOrPhone/index.tsx | 53 ------ .../IdentifierProfileForm}/index.module.scss | 6 +- .../Continue/IdentifierProfileForm/index.tsx | 107 +++++++++++ .../pages/Continue/SetEmail/index.test.tsx | 38 ---- .../ui/src/pages/Continue/SetEmail/index.tsx | 20 -- .../SocialIdentityNotification.tsx | 57 ++++++ .../SetEmailOrPhone/index.module.scss | 6 + .../Continue/SetEmailOrPhone/index.test.tsx | 101 ++++++++++ .../pages/Continue/SetEmailOrPhone/index.tsx | 85 +++++++++ .../pages/Continue/SetPhone/index.test.tsx | 42 ---- .../ui/src/pages/Continue/SetPhone/index.tsx | 22 --- .../pages/Continue/SetUsername/index.test.tsx | 14 +- .../src/pages/Continue/SetUsername/index.tsx | 42 +++- .../Continue}/SetUsername/use-set-username.ts | 0 packages/ui/src/pages/Continue/index.tsx | 21 +- .../ForgotPasswordForm/index.tsx | 5 - .../Register/IdentifierRegisterForm/index.tsx | 7 +- .../SignIn/IdentifierSignInForm/index.tsx | 7 +- .../pages/SignIn/PasswordSignInForm/index.tsx | 7 +- 43 files changed, 419 insertions(+), 1637 deletions(-) delete mode 100644 packages/ui/src/containers/EmailForm/EmailContinue.test.tsx delete mode 100644 packages/ui/src/containers/EmailForm/EmailContinue.tsx delete mode 100644 packages/ui/src/containers/EmailForm/EmailForm.test.tsx delete mode 100644 packages/ui/src/containers/EmailForm/EmailForm.tsx delete mode 100644 packages/ui/src/containers/EmailForm/index.module.scss delete mode 100644 packages/ui/src/containers/EmailForm/index.tsx delete mode 100644 packages/ui/src/containers/PasswordlessSwitch/index.test.tsx delete mode 100644 packages/ui/src/containers/PasswordlessSwitch/index.tsx delete mode 100644 packages/ui/src/containers/PhoneForm/PhoneContinue.test.tsx delete mode 100644 packages/ui/src/containers/PhoneForm/PhoneContinue.tsx delete mode 100644 packages/ui/src/containers/PhoneForm/PhoneForm.test.tsx delete mode 100644 packages/ui/src/containers/PhoneForm/PhoneForm.tsx delete mode 100644 packages/ui/src/containers/PhoneForm/index.module.scss delete mode 100644 packages/ui/src/containers/PhoneForm/index.tsx delete mode 100644 packages/ui/src/containers/UsernameForm/SetUsername/index.test.tsx delete mode 100644 packages/ui/src/containers/UsernameForm/SetUsername/index.tsx delete mode 100644 packages/ui/src/containers/UsernameForm/UsernameForm.test.tsx delete mode 100644 packages/ui/src/containers/UsernameForm/UsernameForm.tsx delete mode 100644 packages/ui/src/containers/UsernameForm/index.ts delete mode 100644 packages/ui/src/hooks/use-form.ts delete mode 100644 packages/ui/src/hooks/use-send-verification-code-legacy.ts delete mode 100644 packages/ui/src/pages/Continue/EmailOrPhone/index.test.tsx delete mode 100644 packages/ui/src/pages/Continue/EmailOrPhone/index.tsx rename packages/ui/src/{containers/UsernameForm => pages/Continue/IdentifierProfileForm}/index.module.scss (69%) create mode 100644 packages/ui/src/pages/Continue/IdentifierProfileForm/index.tsx delete mode 100644 packages/ui/src/pages/Continue/SetEmail/index.test.tsx delete mode 100644 packages/ui/src/pages/Continue/SetEmail/index.tsx create mode 100644 packages/ui/src/pages/Continue/SetEmailOrPhone/SocialIdentityNotification.tsx create mode 100644 packages/ui/src/pages/Continue/SetEmailOrPhone/index.module.scss create mode 100644 packages/ui/src/pages/Continue/SetEmailOrPhone/index.test.tsx create mode 100644 packages/ui/src/pages/Continue/SetEmailOrPhone/index.tsx delete mode 100644 packages/ui/src/pages/Continue/SetPhone/index.test.tsx delete mode 100644 packages/ui/src/pages/Continue/SetPhone/index.tsx rename packages/ui/src/{containers/UsernameForm => pages/Continue}/SetUsername/use-set-username.ts (100%) diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 7a60fe94e..21b50ddb2 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -11,7 +11,6 @@ import initI18n from './i18n/init'; import Callback from './pages/Callback'; import Consent from './pages/Consent'; import Continue from './pages/Continue'; -import ContinueWithEmailOrPhone from './pages/Continue/EmailOrPhone'; import ErrorPage from './pages/ErrorPage'; import ForgotPassword from './pages/ForgotPassword'; import Register from './pages/Register'; @@ -98,7 +97,6 @@ const App = () => { {/* Continue set up missing profile */} - } /> } /> diff --git a/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx b/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx deleted file mode 100644 index 633e545a6..000000000 --- a/packages/ui/src/containers/EmailForm/EmailContinue.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { fireEvent, waitFor, act } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import { putInteraction, sendVerificationCode } from '@/apis/interaction'; - -import EmailContinue from './EmailContinue'; - -const mockedNavigate = jest.fn(); - -jest.mock('@/apis/interaction', () => ({ - sendVerificationCode: jest.fn(() => ({ success: true })), - putInteraction: jest.fn(() => ({ success: true })), -})); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockedNavigate, -})); - -describe('EmailContinue', () => { - const email = 'foo@logto.io'; - - test('register form submit', async () => { - const { container, getByText } = renderWithPageContext( - - - - ); - const emailInput = container.querySelector('input[name="email"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: email } }); - } - - const submitButton = getByText('action.continue'); - - act(() => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(putInteraction).not.toBeCalled(); - expect(sendVerificationCode).toBeCalledWith({ email }); - expect(mockedNavigate).toBeCalledWith( - { pathname: '/continue/email/verification-code', search: '' }, - { state: { email } } - ); - }); - }); -}); diff --git a/packages/ui/src/containers/EmailForm/EmailContinue.tsx b/packages/ui/src/containers/EmailForm/EmailContinue.tsx deleted file mode 100644 index 25c2d4a26..000000000 --- a/packages/ui/src/containers/EmailForm/EmailContinue.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { SignInIdentifier } from '@logto/schemas'; -import { useTranslation } from 'react-i18next'; -import { useLocation } from 'react-router-dom'; -import { is } from 'superstruct'; - -import useSendVerificationCode from '@/hooks/use-send-verification-code-legacy'; -import { UserFlow } from '@/types'; -import { registeredSocialIdentityStateGuard } from '@/types/guard'; -import { maskEmail } from '@/utils/format'; - -import EmailForm from './EmailForm'; -import * as styles from './index.module.scss'; - -type Props = { - className?: string; - // eslint-disable-next-line react/boolean-prop-naming - autoFocus?: boolean; - hasSwitch?: boolean; -}; - -const EmailContinue = (props: Props) => { - const { onSubmit, errorMessage, clearErrorMessage } = useSendVerificationCode( - UserFlow.continue, - SignInIdentifier.Email - ); - const { t } = useTranslation(); - - const { state } = useLocation(); - const hasSocialIdentity = is(state, registeredSocialIdentityStateGuard); - - return ( - <> - - {hasSocialIdentity && state.registeredSocialIdentity?.email && ( -
- {t('description.social_identity_exist', { - type: t('description.email'), - value: maskEmail(state.registeredSocialIdentity.email), - })} -
- )} - - ); -}; - -export default EmailContinue; diff --git a/packages/ui/src/containers/EmailForm/EmailForm.test.tsx b/packages/ui/src/containers/EmailForm/EmailForm.test.tsx deleted file mode 100644 index e926d3038..000000000 --- a/packages/ui/src/containers/EmailForm/EmailForm.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import { fireEvent, waitFor, act } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; - -import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; -import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; - -import EmailForm from './EmailForm'; - -const onSubmit = jest.fn(); -const clearErrorMessage = jest.fn(); - -describe('', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - test('render', () => { - const { queryByText, container } = renderWithPageContext( - - - - ); - expect(container.querySelector('input[name="email"]')).not.toBeNull(); - expect(queryByText('action.continue')).not.toBeNull(); - }); - - test('render with terms settings', () => { - const { queryByText } = renderWithPageContext( - - - - - - ); - expect(queryByText('description.terms_of_use')).not.toBeNull(); - }); - - test('render with terms settings but hasTerms param set to false', () => { - const { queryByText } = renderWithPageContext( - - - - - - ); - expect(queryByText('description.terms_of_use')).toBeNull(); - }); - - test('required email with error message', () => { - const { queryByText, container, getByText } = renderWithPageContext( - - - - ); - const submitButton = getByText('action.continue'); - - fireEvent.click(submitButton); - expect(queryByText('invalid_email')).not.toBeNull(); - expect(onSubmit).not.toBeCalled(); - - const emailInput = container.querySelector('input[name="email"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'foo' } }); - expect(queryByText('invalid_email')).not.toBeNull(); - - fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); - expect(queryByText('invalid_email')).toBeNull(); - } - }); - - test('should display and clear the form error message as expected', () => { - const { queryByText, container } = renderWithPageContext( - - - - ); - - expect(queryByText('form error')).not.toBeNull(); - - const emailInput = container.querySelector('input[name="email"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'foo' } }); - expect(clearErrorMessage).toBeCalled(); - } - }); - - test('should blocked by terms validation with terms settings enabled', async () => { - const { container, getByText } = renderWithPageContext( - - - - - - ); - - const emailInput = container.querySelector('input[name="email"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); - } - - const submitButton = getByText('action.continue'); - - act(() => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(onSubmit).not.toBeCalled(); - }); - }); - - test('should call onSubmit properly with terms settings enabled but hasTerms param set to false', async () => { - const { container, getByText } = renderWithPageContext( - - - - - - ); - - const emailInput = container.querySelector('input[name="email"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); - } - - const submitButton = getByText('action.continue'); - - act(() => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(onSubmit).toBeCalledWith({ email: 'foo@logto.io' }); - }); - }); - - test('should call onSubmit method properly with terms settings enabled and checked', async () => { - const { container, getByText } = renderWithPageContext( - - - - - - ); - const emailInput = container.querySelector('input[name="email"]'); - - if (emailInput) { - fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } }); - } - - const termsButton = getByText('description.agree_with_terms'); - fireEvent.click(termsButton); - - const submitButton = getByText('action.continue'); - - act(() => { - fireEvent.click(submitButton); - }); - - await waitFor(() => { - expect(onSubmit).toBeCalledWith({ email: 'foo@logto.io' }); - }); - }); -}); diff --git a/packages/ui/src/containers/EmailForm/EmailForm.tsx b/packages/ui/src/containers/EmailForm/EmailForm.tsx deleted file mode 100644 index 50ce9f3ff..000000000 --- a/packages/ui/src/containers/EmailForm/EmailForm.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import classNames from 'classnames'; -import { useCallback } from 'react'; -import type { TFuncKey } from 'react-i18next'; -import { useTranslation } from 'react-i18next'; - -import Button from '@/components/Button'; -import ErrorMessage from '@/components/ErrorMessage'; -import Input from '@/components/Input'; -import PasswordlessSwitch from '@/containers/PasswordlessSwitch'; -import TermsOfUse from '@/containers/TermsOfUse'; -import useForm from '@/hooks/use-form'; -import useTerms from '@/hooks/use-terms'; -import { validateEmail } 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; - hasTerms?: boolean; - hasSwitch?: boolean; - errorMessage?: string; - submitButtonText?: TFuncKey; - clearErrorMessage?: () => void; - onSubmit: (payload: { email: string }) => Promise | void; -}; - -type FieldState = { - email: string; -}; - -const defaultState: FieldState = { email: '' }; - -const EmailForm = ({ - autoFocus, - hasTerms = true, - hasSwitch = false, - errorMessage, - className, - submitButtonText = 'action.continue', - clearErrorMessage, - onSubmit, -}: Props) => { - const { t } = useTranslation(); - - const { termsValidation } = useTerms(); - const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState); - - const onSubmitHandler = useCallback( - async (event?: React.FormEvent) => { - event?.preventDefault(); - - if (!validateForm()) { - return; - } - - if (hasTerms && !(await termsValidation())) { - return; - } - - await onSubmit(fieldValue); - }, - [validateForm, hasTerms, termsValidation, onSubmit, fieldValue] - ); - - const { onChange, ...rest } = register('email', validateEmail); - - return ( -
- { - onChange(event); - clearErrorMessage?.(); - }} - {...rest} - onClear={() => { - setFieldValue((state) => ({ ...state, email: '' })); - clearErrorMessage?.(); - }} - /> - {errorMessage && {errorMessage}} - {hasSwitch && } - {hasTerms && } -