diff --git a/packages/ui/src/__mocks__/RenderWithPageContext/SettingsProvider.tsx b/packages/ui/src/__mocks__/RenderWithPageContext/SettingsProvider.tsx index cacae1502..f525b543b 100644 --- a/packages/ui/src/__mocks__/RenderWithPageContext/SettingsProvider.tsx +++ b/packages/ui/src/__mocks__/RenderWithPageContext/SettingsProvider.tsx @@ -3,12 +3,14 @@ import { useContext, useEffect, ReactElement } from 'react'; import { PageContext } from '@/hooks/use-page-context'; import { SignInExperienceSettings } from '@/types'; +import { mockSignInExperienceSettings } from '../logto'; + type Props = { - settings: SignInExperienceSettings; + settings?: SignInExperienceSettings; children: ReactElement; }; -const SettingsProvider = ({ settings, children }: Props) => { +const SettingsProvider = ({ settings = mockSignInExperienceSettings, children }: Props) => { const { setExperienceSettings } = useContext(PageContext); useEffect(() => { diff --git a/packages/ui/src/containers/CreateAccount/index.test.tsx b/packages/ui/src/containers/CreateAccount/index.test.tsx index 829236f3a..12e628bcf 100644 --- a/packages/ui/src/containers/CreateAccount/index.test.tsx +++ b/packages/ui/src/containers/CreateAccount/index.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; import { register } from '@/apis/register'; import CreateAccount from '.'; @@ -15,6 +16,14 @@ describe('', () => { expect(container.querySelector('input[name="password"]')).not.toBeNull(); expect(container.querySelector('input[name="confirm_password"]')).not.toBeNull(); expect(queryByText('action.create')).not.toBeNull(); + }); + + test('render with terms settings enabled', () => { + const { queryByText } = renderWithPageContext( + + + + ); expect(queryByText('description.terms_of_use')).not.toBeNull(); }); @@ -131,8 +140,12 @@ describe('', () => { expect(queryByText('passwords_do_not_match')).toBeNull(); }); - test('submit form properly', async () => { - const { getByText, container } = renderWithPageContext(); + test('submit form properly with terms settings enabled', async () => { + const { getByText, container } = renderWithPageContext( + + + + ); const submitButton = getByText('action.create'); const passwordInput = container.querySelector('input[name="password"]'); const confirmPasswordInput = container.querySelector('input[name="confirm_password"]'); diff --git a/packages/ui/src/containers/CreateAccount/index.tsx b/packages/ui/src/containers/CreateAccount/index.tsx index b7dfa3e18..94c4d243f 100644 --- a/packages/ui/src/containers/CreateAccount/index.tsx +++ b/packages/ui/src/containers/CreateAccount/index.tsx @@ -2,7 +2,6 @@ * TODO: * 1. API redesign handle api error and loading status globally in PageContext * 2. Input field validation, should move the validation rule to the input field scope - * 4. Read terms of use settings from SignInExperience Settings */ import classNames from 'classnames'; @@ -14,9 +13,10 @@ import Button from '@/components/Button'; import { ErrorType } from '@/components/ErrorMessage'; import Input from '@/components/Input'; import PasswordInput from '@/components/Input/PasswordInput'; -import TermsOfUse from '@/components/TermsOfUse'; +import TermsOfUse from '@/containers/TermsOfUse'; import useApi from '@/hooks/use-api'; import { PageContext } from '@/hooks/use-page-context'; +import useTerms from '@/hooks/use-terms'; import * as styles from './index.module.scss'; @@ -24,7 +24,6 @@ type FieldState = { username: string; password: string; confirmPassword: string; - termsAgreement: boolean; }; type ErrorState = { @@ -43,7 +42,6 @@ const defaultState = { username: '', password: '', confirmPassword: '', - termsAgreement: false, }; const usernameRegx = /^[A-Z_a-z-][\w-]*$/; @@ -52,6 +50,7 @@ const CreateAccount = ({ className }: Props) => { const { t, i18n } = useTranslation(undefined, { keyPrefix: 'main_flow' }); const [fieldState, setFieldState] = useState(defaultState); const [fieldErrors, setFieldErrors] = useState({}); + const { termsValidation } = useTerms(); const { setToast } = useContext(PageContext); @@ -86,11 +85,6 @@ const CreateAccount = ({ className }: Props) => { return { code: 'passwords_do_not_match' }; } }, - termsAgreement: ({ termsAgreement }) => { - if (!termsAgreement) { - return 'agree_terms_required'; - } - }, }), [t] ); @@ -118,19 +112,12 @@ const CreateAccount = ({ className }: Props) => { return; } - const termsAgreementError = validations.termsAgreement?.(fieldState); - - if (termsAgreementError) { - setFieldErrors((previous) => ({ - ...previous, - termsAgreement: termsAgreementError, - })); - + if (!termsValidation()) { return; } void asyncRegister(fieldState.username, fieldState.password); - }, [fieldState, validations, asyncRegister]); + }, [validations, fieldState, termsValidation, asyncRegister]); useEffect(() => { if (result?.redirectTo) { @@ -208,15 +195,7 @@ const CreateAccount = ({ className }: Props) => { } }} /> - { - setFieldState((state) => ({ ...state, termsAgreement: checked })); - }} - /> + diff --git a/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx b/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx index e425e5396..d5f81e70b 100644 --- a/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx +++ b/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; import { sendRegisterEmailPasscode } from '@/apis/register'; import { sendSignInEmailPasscode } from '@/apis/sign-in'; @@ -24,6 +25,16 @@ describe('', () => { ); expect(container.querySelector('input[name="email"]')).not.toBeNull(); expect(queryByText('action.continue')).not.toBeNull(); + }); + + test('render with terms settings enabled', () => { + const { queryByText } = renderWithPageContext( + + + + + + ); expect(queryByText('description.terms_of_use')).not.toBeNull(); }); @@ -53,7 +64,9 @@ describe('', () => { test('should call sign-in method properly', async () => { const { container, getByText } = renderWithPageContext( - + + + ); const emailInput = container.querySelector('input[name="email"]'); @@ -76,7 +89,9 @@ describe('', () => { test('should call register method properly', async () => { const { container, getByText } = renderWithPageContext( - + + + ); const emailInput = container.querySelector('input[name="email"]'); diff --git a/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx b/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx index da059036e..cada21b98 100644 --- a/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx +++ b/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx @@ -2,7 +2,6 @@ * TODO: * 1. API redesign handle api error and loading status globally in PageContext * 2. Input field validation, should move the validation rule to the input field scope - * 4. Read terms of use settings from SignInExperience Settings */ import classNames from 'classnames'; import React, { useState, useCallback, useMemo, useEffect, useContext } from 'react'; @@ -13,9 +12,10 @@ import { getSendPasscodeApi } from '@/apis/utils'; import Button from '@/components/Button'; import { ErrorType } from '@/components/ErrorMessage'; import Input from '@/components/Input'; -import TermsOfUse from '@/components/TermsOfUse'; +import TermsOfUse from '@/containers/TermsOfUse'; import useApi from '@/hooks/use-api'; import { PageContext } from '@/hooks/use-page-context'; +import useTerms from '@/hooks/use-terms'; import { UserFlow } from '@/types'; import * as styles from './index.module.scss'; @@ -27,7 +27,6 @@ type Props = { type FieldState = { email: string; - termsAgreement: boolean; }; type ErrorState = { @@ -38,7 +37,7 @@ type FieldValidations = { [key in keyof FieldState]: (state: FieldState) => ErrorType | undefined; }; -const defaultState: FieldState = { email: '', termsAgreement: false }; +const defaultState: FieldState = { email: '' }; const emailRegEx = /^\S+@\S+\.\S+$/; @@ -48,6 +47,7 @@ const EmailPasswordless = ({ type, className }: Props) => { const [fieldErrors, setFieldErrors] = useState({}); const { setToast } = useContext(PageContext); const navigate = useNavigate(); + const { termsValidation } = useTerms(); const sendPasscode = getSendPasscodeApi(type, 'email'); @@ -60,11 +60,6 @@ const EmailPasswordless = ({ type, className }: Props) => { return 'invalid_email'; } }, - termsAgreement: ({ termsAgreement }) => { - if (!termsAgreement) { - return 'agree_terms_required'; - } - }, }), [] ); @@ -78,16 +73,12 @@ const EmailPasswordless = ({ type, className }: Props) => { return; } - const termsAgreementError = validations.termsAgreement(fieldState); - - if (termsAgreementError) { - setFieldErrors((previous) => ({ ...previous, termsAgreement: termsAgreementError })); - + if (!termsValidation()) { return; } void asyncSendPasscode(fieldState.email); - }, [validations, fieldState, asyncSendPasscode]); + }, [validations, fieldState, termsValidation, asyncSendPasscode]); useEffect(() => { if (result) { @@ -137,15 +128,7 @@ const EmailPasswordless = ({ type, className }: Props) => { }} /> - { - setFieldState((state) => ({ ...state, termsAgreement: checked })); - }} - /> + diff --git a/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx b/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx index 9a962e31e..a1564dbbb 100644 --- a/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx +++ b/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; import { sendRegisterSmsPasscode } from '@/apis/register'; import { sendSignInSmsPasscode } from '@/apis/sign-in'; import { defaultCountryCallingCode } from '@/hooks/use-phone-number'; @@ -27,6 +28,16 @@ describe('', () => { ); expect(container.querySelector('input[name="phone"]')).not.toBeNull(); expect(queryByText('action.continue')).not.toBeNull(); + }); + + test('render with terms settings enabled', () => { + const { queryByText } = renderWithPageContext( + + + + + + ); expect(queryByText('description.terms_of_use')).not.toBeNull(); }); @@ -56,7 +67,9 @@ describe('', () => { test('should call sign-in method properly', async () => { const { container, getByText } = renderWithPageContext( - + + + ); const phoneInput = container.querySelector('input[name="phone"]'); @@ -79,7 +92,9 @@ describe('', () => { test('should call register method properly', async () => { const { container, getByText } = renderWithPageContext( - + + + ); const phoneInput = container.querySelector('input[name="phone"]'); diff --git a/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx b/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx index 8187235b1..f8b9a37b0 100644 --- a/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx +++ b/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx @@ -2,7 +2,6 @@ * TODO: * 1. API redesign handle api error and loading status globally in PageContext * 2. Input field validation, should move the validation rule to the input field scope - * 4. Read terms of use settings from SignInExperience Settings */ import classNames from 'classnames'; import React, { useState, useCallback, useMemo, useEffect, useContext } from 'react'; @@ -13,10 +12,11 @@ import { getSendPasscodeApi } from '@/apis/utils'; import Button from '@/components/Button'; import { ErrorType } from '@/components/ErrorMessage'; import PhoneInput from '@/components/Input/PhoneInput'; -import TermsOfUse from '@/components/TermsOfUse'; +import TermsOfUse from '@/containers/TermsOfUse'; import useApi from '@/hooks/use-api'; import { PageContext } from '@/hooks/use-page-context'; import usePhoneNumber, { countryList } from '@/hooks/use-phone-number'; +import useTerms from '@/hooks/use-terms'; import { UserFlow } from '@/types'; import * as styles from './index.module.scss'; @@ -28,7 +28,6 @@ type Props = { type FieldState = { phone: string; - termsAgreement: boolean; }; type ErrorState = { @@ -39,7 +38,7 @@ type FieldValidations = { [key in keyof FieldState]: (state: FieldState) => ErrorType | undefined; }; -const defaultState: FieldState = { phone: '', termsAgreement: false }; +const defaultState: FieldState = { phone: '' }; const PhonePasswordless = ({ type, className }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); @@ -47,6 +46,7 @@ const PhonePasswordless = ({ type, className }: Props) => { const [fieldErrors, setFieldErrors] = useState({}); const { setToast } = useContext(PageContext); const navigate = useNavigate(); + const { termsValidation } = useTerms(); const { phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber(); @@ -60,11 +60,6 @@ const PhonePasswordless = ({ type, className }: Props) => { return 'invalid_phone'; } }, - termsAgreement: ({ termsAgreement }) => { - if (!termsAgreement) { - return 'agree_terms_required'; - } - }, }), [isValidPhoneNumber] ); @@ -78,16 +73,12 @@ const PhonePasswordless = ({ type, className }: Props) => { return; } - const termsAgreementError = validations.termsAgreement(fieldState); - - if (termsAgreementError) { - setFieldErrors((previous) => ({ ...previous, termsAgreement: termsAgreementError })); - + if (!termsValidation()) { return; } void asyncSendPasscode(fieldState.phone); - }, [validations, fieldState, asyncSendPasscode]); + }, [validations, fieldState, termsValidation, asyncSendPasscode]); useEffect(() => { setFieldState((previous) => ({ @@ -97,8 +88,6 @@ const PhonePasswordless = ({ type, className }: Props) => { }, [phoneNumber]); useEffect(() => { - console.log(result); - if (result) { navigate(`/${type}/sms/passcode-validation`, { state: { phone: fieldState.phone } }); } @@ -140,15 +129,7 @@ const PhonePasswordless = ({ type, className }: Props) => { setPhoneNumber((previous) => ({ ...previous, ...data })); }} /> - { - setFieldState((state) => ({ ...state, termsAgreement: checked })); - }} - /> + diff --git a/packages/ui/src/containers/TermsOfUse/intext.test.tsx b/packages/ui/src/containers/TermsOfUse/intext.test.tsx index c0392dad6..b77bdbbaa 100644 --- a/packages/ui/src/containers/TermsOfUse/intext.test.tsx +++ b/packages/ui/src/containers/TermsOfUse/intext.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; -import { mockSignInExperienceSettings } from '@/__mocks__/logto'; import TermsOfUse from '.'; @@ -14,7 +13,7 @@ describe('TermsOfUse Container', () => { it('render with settings', async () => { const { queryByText } = renderWithPageContext( - + ); diff --git a/packages/ui/src/containers/UsernameSignin/index.test.tsx b/packages/ui/src/containers/UsernameSignin/index.test.tsx index fdc971c4b..7f2dc4be9 100644 --- a/packages/ui/src/containers/UsernameSignin/index.test.tsx +++ b/packages/ui/src/containers/UsernameSignin/index.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, waitFor } from '@testing-library/react'; import React from 'react'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; import { signInBasic } from '@/apis/sign-in'; import UsernameSignin from '.'; @@ -14,6 +15,14 @@ describe('', () => { expect(container.querySelector('input[name="username"]')).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(); }); @@ -41,15 +50,15 @@ describe('', () => { fireEvent.change(passwordInput, { target: { value: 'password' } }); } - fireEvent.click(submitButton); - expect(queryByText('required')).toBeNull(); - - expect(signInBasic).not.toBeCalled(); }); test('submit form', async () => { - const { getByText, container } = renderWithPageContext(); + const { getByText, container } = renderWithPageContext( + + + + ); const submitButton = getByText('action.sign_in'); const usernameInput = container.querySelector('input[name="username"]'); diff --git a/packages/ui/src/containers/UsernameSignin/index.tsx b/packages/ui/src/containers/UsernameSignin/index.tsx index 8c562adb9..d27e9cfad 100644 --- a/packages/ui/src/containers/UsernameSignin/index.tsx +++ b/packages/ui/src/containers/UsernameSignin/index.tsx @@ -2,7 +2,6 @@ * TODO: * 1. API redesign handle api error and loading status globally in PageContext * 2. Input field validation, should move the validation rule to the input field scope - * 4. Read terms of use settings from SignInExperience Settings */ import classNames from 'classnames'; @@ -14,16 +13,16 @@ import Button from '@/components/Button'; import { ErrorType } from '@/components/ErrorMessage'; import Input from '@/components/Input'; import PasswordInput from '@/components/Input/PasswordInput'; -import TermsOfUse from '@/components/TermsOfUse'; +import TermsOfUse from '@/containers/TermsOfUse'; import useApi from '@/hooks/use-api'; import { PageContext } from '@/hooks/use-page-context'; +import useTerms from '@/hooks/use-terms'; import * as styles from './index.module.scss'; type FieldState = { username: string; password: string; - termsAgreement: boolean; }; type ErrorState = { @@ -41,17 +40,15 @@ type Props = { const defaultState: FieldState = { username: '', password: '', - termsAgreement: false, }; const UsernameSignin = ({ className }: Props) => { const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' }); const [fieldState, setFieldState] = useState(defaultState); const [fieldErrors, setFieldErrors] = useState({}); - const { setToast } = useContext(PageContext); - const { error, result, run: asyncSignInBasic } = useApi(signInBasic); + const { termsValidation } = useTerms(); const validations = useMemo( () => ({ @@ -65,11 +62,6 @@ const UsernameSignin = ({ className }: Props) => { return { code: 'required', data: { field: t('input.password') } }; } }, - termsAgreement: ({ termsAgreement }) => { - if (!termsAgreement) { - return 'agree_terms_required'; - } - }, }), [t] ); @@ -89,19 +81,12 @@ const UsernameSignin = ({ className }: Props) => { return; } - const termsAgreementError = validations.termsAgreement?.(fieldState); - - if (termsAgreementError) { - setFieldErrors((previous) => ({ - ...previous, - termsAgreement: termsAgreementError, - })); - + if (!termsValidation()) { return; } void asyncSignInBasic(fieldState.username, fieldState.password); - }, [validations, fieldState, asyncSignInBasic]); + }, [validations, fieldState, asyncSignInBasic, termsValidation]); useEffect(() => { if (result?.redirectTo) { @@ -165,15 +150,7 @@ const UsernameSignin = ({ className }: Props) => { }} /> - { - setFieldState((state) => ({ ...state, termsAgreement: checked })); - }} - /> + diff --git a/packages/ui/src/hooks/use-social.ts b/packages/ui/src/hooks/use-social.ts index f555d7059..10a272944 100644 --- a/packages/ui/src/hooks/use-social.ts +++ b/packages/ui/src/hooks/use-social.ts @@ -6,6 +6,7 @@ import { generateRandomString, parseQueryParameters } from '@/utils'; import useApi from './use-api'; import { PageContext } from './use-page-context'; +import useTerms from './use-terms'; /** * Social Connector State Utility Methods @@ -65,6 +66,7 @@ const isNativeWebview = () => { const useSocial = () => { const { setToast } = useContext(PageContext); + const { termsValidation } = useTerms(); const parameters = useParams(); const { result: invokeSocialSignInResult, run: asyncInvokeSocialSignIn } = @@ -74,6 +76,10 @@ const useSocial = () => { const invokeSocialSignInHandler = useCallback( async (connectorId: string) => { + if (!termsValidation()) { + return; + } + const state = generateState(); storeState(state, connectorId); @@ -81,7 +87,7 @@ const useSocial = () => { return asyncInvokeSocialSignIn(connectorId, state, `${origin}/callback/${connectorId}`); }, - [asyncInvokeSocialSignIn] + [asyncInvokeSocialSignIn, termsValidation] ); const signInWithSocialHandler = useCallback( @@ -166,7 +172,7 @@ const useSocial = () => { } }, [signInWithSocialResult]); - // SignIn Callback Page Handler + // Social Sign-In Callback Handler useEffect(() => { if (!location.pathname.includes('/sign-in/callback') || !parameters.connector) { return; diff --git a/packages/ui/src/hooks/use-terms.ts b/packages/ui/src/hooks/use-terms.ts index b822c81cc..b2d69f60e 100644 --- a/packages/ui/src/hooks/use-terms.ts +++ b/packages/ui/src/hooks/use-terms.ts @@ -12,12 +12,14 @@ const useTerms = () => { } = useContext(PageContext); const termsValidation = useCallback(() => { - if (termsAgreement) { - return; + if (termsAgreement || !experienceSettings?.termsOfUse.enabled) { + return true; } setShowTermsModal(true); - }, [setShowTermsModal, termsAgreement]); + + return false; + }, [experienceSettings, termsAgreement, setShowTermsModal]); return { termsSettings: experienceSettings?.termsOfUse,