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,