0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-03 21:48:55 -05:00

refactor(ui): replace termsOfUse Component with Container (#610)

* refactor(ui): refactor terms of use

refactor terms of use

* fix(ui): fix terms modal

fix terms mdoal

* refactor(ui): replace termsOfUse Component with Container

replace termsOfUse Component With Container
This commit is contained in:
simeng-li 2022-04-22 14:01:43 +08:00 committed by GitHub
parent 3d8c3af5bd
commit 666c5d8b8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 107 additions and 126 deletions

View file

@ -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(() => {

View file

@ -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('<CreateAccount/>', () => {
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(
<SettingsProvider>
<CreateAccount />
</SettingsProvider>
);
expect(queryByText('description.terms_of_use')).not.toBeNull();
});
@ -131,8 +140,12 @@ describe('<CreateAccount/>', () => {
expect(queryByText('passwords_do_not_match')).toBeNull();
});
test('submit form properly', async () => {
const { getByText, container } = renderWithPageContext(<CreateAccount />);
test('submit form properly with terms settings enabled', async () => {
const { getByText, container } = renderWithPageContext(
<SettingsProvider>
<CreateAccount />
</SettingsProvider>
);
const submitButton = getByText('action.create');
const passwordInput = container.querySelector('input[name="password"]');
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');

View file

@ -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<FieldState>(defaultState);
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
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) => {
}
}}
/>
<TermsOfUse
name="termsAgreement"
className={styles.terms}
termsUrl="/"
isChecked={fieldState.termsAgreement}
onChange={(checked) => {
setFieldState((state) => ({ ...state, termsAgreement: checked }));
}}
/>
<TermsOfUse className={styles.terms} />
<Button onClick={onSubmitHandler}>{t('action.create')}</Button>
</form>

View file

@ -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('<EmailPasswordless/>', () => {
);
expect(container.querySelector('input[name="email"]')).not.toBeNull();
expect(queryByText('action.continue')).not.toBeNull();
});
test('render with terms settings enabled', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<EmailPasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('description.terms_of_use')).not.toBeNull();
});
@ -53,7 +64,9 @@ describe('<EmailPasswordless/>', () => {
test('should call sign-in method properly', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="sign-in" />
<SettingsProvider>
<EmailPasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
const emailInput = container.querySelector('input[name="email"]');
@ -76,7 +89,9 @@ describe('<EmailPasswordless/>', () => {
test('should call register method properly', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="register" />
<SettingsProvider>
<EmailPasswordless type="register" />
</SettingsProvider>
</MemoryRouter>
);
const emailInput = container.querySelector('input[name="email"]');

View file

@ -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<ErrorState>({});
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) => {
}}
/>
<TermsOfUse
name="termsAgreement"
className={styles.terms}
termsUrl="/"
isChecked={fieldState.termsAgreement}
onChange={(checked) => {
setFieldState((state) => ({ ...state, termsAgreement: checked }));
}}
/>
<TermsOfUse className={styles.terms} />
<Button onClick={onSubmitHandler}>{t('action.continue')}</Button>
</form>

View file

@ -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('<PhonePasswordless/>', () => {
);
expect(container.querySelector('input[name="phone"]')).not.toBeNull();
expect(queryByText('action.continue')).not.toBeNull();
});
test('render with terms settings enabled', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<PhonePasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('description.terms_of_use')).not.toBeNull();
});
@ -56,7 +67,9 @@ describe('<PhonePasswordless/>', () => {
test('should call sign-in method properly', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="sign-in" />
<SettingsProvider>
<PhonePasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
const phoneInput = container.querySelector('input[name="phone"]');
@ -79,7 +92,9 @@ describe('<PhonePasswordless/>', () => {
test('should call register method properly', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="register" />
<SettingsProvider>
<PhonePasswordless type="register" />
</SettingsProvider>
</MemoryRouter>
);
const phoneInput = container.querySelector('input[name="phone"]');

View file

@ -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<ErrorState>({});
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 }));
}}
/>
<TermsOfUse
name="termsAgreement"
className={styles.terms}
termsUrl="/"
isChecked={fieldState.termsAgreement}
onChange={(checked) => {
setFieldState((state) => ({ ...state, termsAgreement: checked }));
}}
/>
<TermsOfUse className={styles.terms} />
<Button onClick={onSubmitHandler}>{t('action.continue')}</Button>
</form>

View file

@ -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(
<SettingsProvider settings={mockSignInExperienceSettings}>
<SettingsProvider>
<TermsOfUse />
</SettingsProvider>
);

View file

@ -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('<UsernameSignin>', () => {
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(
<SettingsProvider>
<UsernameSignin />
</SettingsProvider>
);
expect(queryByText('description.agree_with_terms')).not.toBeNull();
});
@ -41,15 +50,15 @@ describe('<UsernameSignin>', () => {
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(<UsernameSignin />);
const { getByText, container } = renderWithPageContext(
<SettingsProvider>
<UsernameSignin />
</SettingsProvider>
);
const submitButton = getByText('action.sign_in');
const usernameInput = container.querySelector('input[name="username"]');

View file

@ -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<FieldState>(defaultState);
const [fieldErrors, setFieldErrors] = useState<ErrorState>({});
const { setToast } = useContext(PageContext);
const { error, result, run: asyncSignInBasic } = useApi(signInBasic);
const { termsValidation } = useTerms();
const validations = useMemo<FieldValidations>(
() => ({
@ -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) => {
}}
/>
<TermsOfUse
name="termsAgreement"
className={styles.terms}
termsUrl="/"
isChecked={fieldState.termsAgreement}
onChange={(checked) => {
setFieldState((state) => ({ ...state, termsAgreement: checked }));
}}
/>
<TermsOfUse className={styles.terms} />
<Button onClick={onSubmitHandler}>{t('action.sign_in')}</Button>
</form>

View file

@ -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;

View file

@ -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,