diff --git a/packages/core/src/routes/well-known.ts b/packages/core/src/routes/well-known.ts index 8ef4650dc..886e3d251 100644 --- a/packages/core/src/routes/well-known.ts +++ b/packages/core/src/routes/well-known.ts @@ -37,6 +37,15 @@ export default function wellKnownRoutes(router: T, pr getLogtoConnectors(), ]); + const forgotPassword = { + sms: logtoConnectors.some( + ({ type, dbEntry: { enabled } }) => type === ConnectorType.Sms && enabled + ), + email: logtoConnectors.some( + ({ type, dbEntry: { enabled } }) => type === ConnectorType.Email && enabled + ), + }; + // Hard code AdminConsole sign-in methods settings. if (interaction?.params.client_id === adminConsoleApplicationId) { ctx.body = { @@ -48,6 +57,7 @@ export default function wellKnownRoutes(router: T, pr languageInfo: signInExperience.languageInfo, signInMode: (await hasActiveUsers()) ? SignInMode.SignIn : SignInMode.Register, socialConnectors: [], + forgotPassword, }; return next(); @@ -81,6 +91,7 @@ export default function wellKnownRoutes(router: T, pr 'demo_app.notification', autoDetect ? undefined : { lng: fallbackLanguage } ), + forgotPassword, }; return next(); @@ -89,14 +100,7 @@ export default function wellKnownRoutes(router: T, pr ctx.body = { ...signInExperience, socialConnectors, - forgotPassword: { - sms: logtoConnectors.some( - ({ type, dbEntry: { enabled } }) => type === ConnectorType.Sms && enabled - ), - email: logtoConnectors.some( - ({ type, dbEntry: { enabled } }) => type === ConnectorType.Email && enabled - ), - }, + forgotPassword, }; return next(); diff --git a/packages/ui/src/components/ForgotPasswordLink/index.tsx b/packages/ui/src/components/ForgotPasswordLink/index.tsx new file mode 100644 index 000000000..0b2a254c0 --- /dev/null +++ b/packages/ui/src/components/ForgotPasswordLink/index.tsx @@ -0,0 +1,19 @@ +import type { SignInIdentifier } from '@logto/schemas'; + +import TextLink from '@/components/TextLink'; +import { UserFlow } from '@/types'; + +type Props = { + method: SignInIdentifier.Email | SignInIdentifier.Sms; + className?: string; +}; + +const ForgotPasswordLink = ({ method, className }: Props) => ( + +); + +export default ForgotPasswordLink; diff --git a/packages/ui/src/containers/EmailForm/index.module.scss b/packages/ui/src/containers/EmailForm/index.module.scss index f73c1471a..b7d9e3406 100644 --- a/packages/ui/src/containers/EmailForm/index.module.scss +++ b/packages/ui/src/containers/EmailForm/index.module.scss @@ -14,7 +14,8 @@ } .switch { - display: block; + width: auto; + align-self: start; } .formErrors { diff --git a/packages/ui/src/containers/EmailPassword/index.module.scss b/packages/ui/src/containers/EmailPassword/index.module.scss index fe937e867..a2028eed0 100644 --- a/packages/ui/src/containers/EmailPassword/index.module.scss +++ b/packages/ui/src/containers/EmailPassword/index.module.scss @@ -8,10 +8,16 @@ } .inputField, + .link, .terms { margin-bottom: _.unit(4); } + .link { + width: auto; + align-self: start; + } + .formErrors { margin-top: _.unit(-2); margin-bottom: _.unit(4); diff --git a/packages/ui/src/containers/EmailPassword/index.test.tsx b/packages/ui/src/containers/EmailPassword/index.test.tsx index 758f12ae4..2eb143706 100644 --- a/packages/ui/src/containers/EmailPassword/index.test.tsx +++ b/packages/ui/src/containers/EmailPassword/index.test.tsx @@ -1,5 +1,6 @@ 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'; @@ -28,11 +29,14 @@ describe('', () => { 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', () => { @@ -64,11 +68,13 @@ describe('', () => { test('should show terms confirm modal', async () => { const { queryByText, getByText, container } = renderWithPageContext( - - - - - + + + + + + + ); const submitButton = getByText('action.sign_in'); @@ -94,11 +100,13 @@ describe('', () => { test('should show terms detail modal', async () => { const { getByText, queryByText, container, queryByRole } = renderWithPageContext( - - - - - + + + + + + + ); const submitButton = getByText('action.sign_in'); @@ -135,9 +143,11 @@ describe('', () => { test('submit form', async () => { const { getByText, container } = renderWithPageContext( - - - + + + + + ); const submitButton = getByText('action.sign_in'); diff --git a/packages/ui/src/containers/EmailPassword/index.tsx b/packages/ui/src/containers/EmailPassword/index.tsx index 0373843d9..25619ca44 100644 --- a/packages/ui/src/containers/EmailPassword/index.tsx +++ b/packages/ui/src/containers/EmailPassword/index.tsx @@ -5,10 +5,12 @@ 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'; @@ -34,6 +36,7 @@ const EmailPassword = ({ className, autoFocus }: Props) => { const { t } = useTranslation(); const { termsValidation } = useTerms(); const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(SignInIdentifier.Email); + const { isForgotPasswordEnabled, email } = useForgotPasswordSettings(); const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState); @@ -86,6 +89,13 @@ const EmailPassword = ({ className, autoFocus }: Props) => { {...register('password', (value) => requiredValidation('password', value))} /> + {isForgotPasswordEnabled && ( + + )} + {errorMessage && {errorMessage}} diff --git a/packages/ui/src/containers/PasswordSignInForm/index.module.scss b/packages/ui/src/containers/PasswordSignInForm/index.module.scss index 0dc3288ee..43be90694 100644 --- a/packages/ui/src/containers/PasswordSignInForm/index.module.scss +++ b/packages/ui/src/containers/PasswordSignInForm/index.module.scss @@ -8,12 +8,15 @@ } .inputField, + .link, .switch { margin-bottom: _.unit(4); } + .link, .switch { - display: block; + align-self: start; + width: auto; } .formErrors { diff --git a/packages/ui/src/containers/PasswordSignInForm/index.tsx b/packages/ui/src/containers/PasswordSignInForm/index.tsx index ae66fce4f..b2dfd9f2f 100644 --- a/packages/ui/src/containers/PasswordSignInForm/index.tsx +++ b/packages/ui/src/containers/PasswordSignInForm/index.tsx @@ -1,13 +1,15 @@ -import type { SignInIdentifier } from '@logto/schemas'; +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 { PasswordInput } from '@/components/Input'; import useForm from '@/hooks/use-form'; import usePasswordSignIn from '@/hooks/use-password-sign-in'; +import { useForgotPasswordSettings } from '@/hooks/use-sie'; import { requiredValidation } from '@/utils/field-validations'; import PasswordlessSignInLink from './PasswordlessSignInLink'; @@ -42,6 +44,8 @@ const PasswordSignInForm = ({ const { fieldValue, register, validateForm } = useForm(defaultState); + const { isForgotPasswordEnabled, sms, email } = useForgotPasswordSettings(); + const onSubmitHandler = useCallback( async (event?: React.FormEvent) => { event?.preventDefault(); @@ -69,6 +73,21 @@ const PasswordSignInForm = ({ /> {errorMessage && {errorMessage}} + {isForgotPasswordEnabled && ( + + )} + {hasPasswordlessButton && ( )} diff --git a/packages/ui/src/containers/PhoneForm/index.module.scss b/packages/ui/src/containers/PhoneForm/index.module.scss index f73c1471a..c58c38cd6 100644 --- a/packages/ui/src/containers/PhoneForm/index.module.scss +++ b/packages/ui/src/containers/PhoneForm/index.module.scss @@ -14,7 +14,8 @@ } .switch { - display: block; + align-self: start; + width: auto; } .formErrors { diff --git a/packages/ui/src/containers/PhonePassword/index.module.scss b/packages/ui/src/containers/PhonePassword/index.module.scss index fe937e867..302bd1775 100644 --- a/packages/ui/src/containers/PhonePassword/index.module.scss +++ b/packages/ui/src/containers/PhonePassword/index.module.scss @@ -8,10 +8,16 @@ } .inputField, + .link, .terms { margin-bottom: _.unit(4); } + .link { + align-self: start; + width: auto; + } + .formErrors { margin-top: _.unit(-2); margin-bottom: _.unit(4); diff --git a/packages/ui/src/containers/PhonePassword/index.test.tsx b/packages/ui/src/containers/PhonePassword/index.test.tsx index 767822325..4dc7ea7e3 100644 --- a/packages/ui/src/containers/PhonePassword/index.test.tsx +++ b/packages/ui/src/containers/PhonePassword/index.test.tsx @@ -1,5 +1,6 @@ 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'; @@ -35,11 +36,14 @@ describe('', () => { 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', () => { @@ -71,11 +75,13 @@ describe('', () => { test('should show terms confirm modal', async () => { const { queryByText, getByText, container } = renderWithPageContext( - - - - - + + + + + + + ); const submitButton = getByText('action.sign_in'); @@ -101,11 +107,13 @@ describe('', () => { test('should show terms detail modal', async () => { const { getByText, queryByText, container, queryByRole } = renderWithPageContext( - - - - - + + + + + + + ); const submitButton = getByText('action.sign_in'); @@ -142,9 +150,11 @@ describe('', () => { test('submit form', async () => { const { getByText, container } = renderWithPageContext( - - - + + + + + ); const submitButton = getByText('action.sign_in'); diff --git a/packages/ui/src/containers/PhonePassword/index.tsx b/packages/ui/src/containers/PhonePassword/index.tsx index 310e3bc1a..e16b7e338 100644 --- a/packages/ui/src/containers/PhonePassword/index.tsx +++ b/packages/ui/src/containers/PhonePassword/index.tsx @@ -5,11 +5,13 @@ import { useTranslation } from 'react-i18next'; import Button from '@/components/Button'; import ErrorMessage from '@/components/ErrorMessage'; +import ForgotPasswordLink from '@/components/ForgotPasswordLink'; import { PhoneInput, PasswordInput } from '@/components/Input'; import TermsOfUse from '@/containers/TermsOfUse'; import useForm from '@/hooks/use-form'; import usePasswordSignIn from '@/hooks/use-password-sign-in'; import usePhoneNumber from '@/hooks/use-phone-number'; +import { useForgotPasswordSettings } from '@/hooks/use-sie'; import useTerms from '@/hooks/use-terms'; import { requiredValidation } from '@/utils/field-validations'; @@ -35,6 +37,7 @@ const PhonePassword = ({ className, autoFocus }: Props) => { const { t } = useTranslation(); const { termsValidation } = useTerms(); const { errorMessage, clearErrorMessage, onSubmit } = usePasswordSignIn(SignInIdentifier.Sms); + const { isForgotPasswordEnabled, sms } = useForgotPasswordSettings(); const { countryList, phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber(); const { fieldValue, setFieldValue, register, validateForm } = useForm(defaultState); @@ -108,6 +111,13 @@ const PhonePassword = ({ className, autoFocus }: Props) => { {errorMessage && {errorMessage}} + {isForgotPasswordEnabled && ( + + )} +