mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
refactor(ui): refactor ui terms of use flow (#1259)
* refactor(ui): refactor ui terms of use flow refactor ui terms of use flow * test(terms): add ut add ut
This commit is contained in:
parent
6148bb0add
commit
f765ccf8cd
13 changed files with 241 additions and 108 deletions
|
@ -135,7 +135,7 @@ export const mockSignInExperience: SignInExperience = {
|
|||
},
|
||||
termsOfUse: {
|
||||
enabled: true,
|
||||
contentUrl: 'http://terms.of.use',
|
||||
contentUrl: 'http://terms.of.use/',
|
||||
},
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
|
||||
import TermsOfUseModal from '.';
|
||||
|
||||
describe('TermsOfUseModal', () => {
|
||||
|
@ -8,13 +9,8 @@ describe('TermsOfUseModal', () => {
|
|||
const onCancel = jest.fn();
|
||||
|
||||
it('render properly', () => {
|
||||
const { queryByText } = render(
|
||||
<TermsOfUseModal
|
||||
isOpen
|
||||
termsUrl="https://www.google.com"
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<TermsOfUseModal isOpen onConfirm={onConfirm} onClose={onCancel} />
|
||||
);
|
||||
|
||||
expect(queryByText('description.agree_with_terms_modal')).not.toBeNull();
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import React, { ReactNode, useContext } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { create } from 'react-modal-promise';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
import { WebModal as ConfirmModal } from '@/components/ConfirmModal';
|
||||
import { WebModal, MobileModal } from '@/components/ConfirmModal';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import usePlatform from '@/hooks/use-platform';
|
||||
import { TermsOfUseModalMessage } from '@/types';
|
||||
|
||||
import { modalPromisify } from '../termsOfUseModalPromisify';
|
||||
|
||||
/**
|
||||
* For web use only confirm modal, does not contain Terms iframe
|
||||
|
@ -12,25 +18,43 @@ import TextLink from '@/components/TextLink';
|
|||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
termsUrl: string;
|
||||
onClose: (message?: TermsOfUseModalMessage) => void;
|
||||
};
|
||||
|
||||
const TermsOfUseConfirmModal = ({ isOpen = false, termsUrl, onConfirm, onClose }: Props) => {
|
||||
const TermsOfUseConfirmModal = ({ isOpen = false, onConfirm, onClose }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile } = usePlatform();
|
||||
const { setTermsAgreement, experienceSettings } = useContext(PageContext);
|
||||
const { termsOfUse } = experienceSettings ?? {};
|
||||
|
||||
const ConfirmModal = isMobile ? MobileModal : WebModal;
|
||||
|
||||
const terms = t('description.terms_of_use');
|
||||
const content = t('description.agree_with_terms_modal', { terms });
|
||||
|
||||
const linkProps = isMobile
|
||||
? {
|
||||
onClick: () => {
|
||||
onClose(TermsOfUseModalMessage.SHOW_DETAIL_MODAL);
|
||||
},
|
||||
}
|
||||
: {
|
||||
href: termsOfUse?.contentUrl,
|
||||
target: '_blank',
|
||||
};
|
||||
|
||||
const modalContent: ReactNode = reactStringReplace(content, terms, () => (
|
||||
<TextLink key={terms} text="description.terms_of_use" href={termsUrl} target="_blank" />
|
||||
<TextLink key={terms} text="description.terms_of_use" {...linkProps} />
|
||||
));
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
confirmText="action.agree"
|
||||
onConfirm={onConfirm}
|
||||
onConfirm={() => {
|
||||
setTermsAgreement(true);
|
||||
onConfirm();
|
||||
}}
|
||||
onClose={onClose}
|
||||
>
|
||||
{modalContent}
|
||||
|
@ -39,3 +63,5 @@ const TermsOfUseConfirmModal = ({ isOpen = false, termsUrl, onConfirm, onClose }
|
|||
};
|
||||
|
||||
export default TermsOfUseConfirmModal;
|
||||
|
||||
export const termsOfUseConfirmModalPromise = create(modalPromisify(TermsOfUseConfirmModal));
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||
|
||||
import TermsOfUseIframeModal from '.';
|
||||
|
||||
describe('TermsOfUseModal', () => {
|
||||
|
@ -8,13 +12,10 @@ describe('TermsOfUseModal', () => {
|
|||
const onCancel = jest.fn();
|
||||
|
||||
it('render properly', () => {
|
||||
const { queryByText } = render(
|
||||
<TermsOfUseIframeModal
|
||||
isOpen
|
||||
termsUrl="https://www.google.com/"
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
const { queryByText, getByText } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<TermsOfUseIframeModal isOpen onConfirm={onConfirm} onClose={onCancel} />
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
expect(queryByText('action.agree')).not.toBeNull();
|
||||
|
@ -24,7 +25,13 @@ describe('TermsOfUseModal', () => {
|
|||
expect(iframe).not.toBeNull();
|
||||
|
||||
if (iframe) {
|
||||
expect(iframe).toHaveProperty('src', 'https://www.google.com/');
|
||||
expect(iframe).toHaveProperty('src', mockSignInExperienceSettings.termsOfUse.contentUrl);
|
||||
}
|
||||
|
||||
const confirmButton = getByText('action.agree');
|
||||
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
expect(onConfirm).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { create } from 'react-modal-promise';
|
||||
|
||||
import { IframeModal } from '@/components/ConfirmModal';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
|
||||
import { modalPromisify } from '../termsOfUseModalPromisify';
|
||||
|
||||
/**
|
||||
* For mobile use only, includes embedded Terms iframe
|
||||
|
@ -9,19 +13,26 @@ type Props = {
|
|||
isOpen?: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
termsUrl: string;
|
||||
};
|
||||
|
||||
const TermsOfUseIframeModal = ({ isOpen = false, termsUrl, onConfirm, onClose }: Props) => {
|
||||
const TermsOfUseIframeModal = ({ isOpen = false, onConfirm, onClose }: Props) => {
|
||||
const { setTermsAgreement, experienceSettings } = useContext(PageContext);
|
||||
const { termsOfUse } = experienceSettings ?? {};
|
||||
|
||||
return (
|
||||
<IframeModal
|
||||
isOpen={isOpen}
|
||||
confirmText="action.agree"
|
||||
url={termsUrl}
|
||||
onConfirm={onConfirm}
|
||||
url={termsOfUse?.contentUrl ?? ''}
|
||||
onConfirm={() => {
|
||||
setTermsAgreement(true);
|
||||
onConfirm();
|
||||
}}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfUseIframeModal;
|
||||
|
||||
export const termsOfUseIframeModalPromise = create(modalPromisify(TermsOfUseIframeModal));
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
|
||||
import TermsOfUsePromiseModal from '.';
|
||||
|
||||
describe('TermsOfUsePromiseModal', () => {
|
||||
it('render properly', () => {
|
||||
const onResolve = jest.fn();
|
||||
const onReject = jest.fn();
|
||||
|
||||
const { getByText } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<TermsOfUsePromiseModal
|
||||
isOpen
|
||||
open
|
||||
instanceId="1"
|
||||
close={jest.fn()}
|
||||
onReject={onReject}
|
||||
onResolve={onResolve}
|
||||
/>
|
||||
</SettingsProvider>
|
||||
);
|
||||
|
||||
const confirmButton = getByText('action.agree');
|
||||
const cancelButton = getByText('action.cancel');
|
||||
|
||||
fireEvent.click(confirmButton);
|
||||
expect(onResolve).toBeCalledWith(true);
|
||||
|
||||
fireEvent.click(cancelButton);
|
||||
expect(onReject).toBeCalledWith(false);
|
||||
});
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { create, InstanceProps } from 'react-modal-promise';
|
||||
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import usePlatform from '@/hooks/use-platform';
|
||||
|
||||
import TermsOfUseConfirmModal from '../TermsOfUseConfirmModal';
|
||||
import TermsOfUseIframeModal from '../TermsOfUseIframeModal';
|
||||
|
||||
const TermsOfUsePromiseModal = ({ isOpen, onResolve, onReject }: InstanceProps<boolean>) => {
|
||||
const { setTermsAgreement, experienceSettings } = useContext(PageContext);
|
||||
const { termsOfUse } = experienceSettings ?? {};
|
||||
const { isMobile } = usePlatform();
|
||||
|
||||
const ConfirmModal = isMobile ? TermsOfUseIframeModal : TermsOfUseConfirmModal;
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
termsUrl={termsOfUse?.contentUrl ?? ''}
|
||||
onConfirm={() => {
|
||||
setTermsAgreement(true);
|
||||
onResolve(true);
|
||||
}}
|
||||
onClose={() => {
|
||||
onReject(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfUsePromiseModal;
|
||||
|
||||
export const termsOfUseModalPromise = create(TermsOfUsePromiseModal);
|
|
@ -10,7 +10,8 @@ type Props = {
|
|||
};
|
||||
|
||||
const TermsOfUse = ({ className }: Props) => {
|
||||
const { termsAgreement, setTermsAgreement, termsSettings, termsOfUserModalHandler } = useTerms();
|
||||
const { termsAgreement, setTermsAgreement, termsSettings, termsOfUseIframeModalHandler } =
|
||||
useTerms();
|
||||
const { isMobile } = usePlatform();
|
||||
|
||||
if (!termsSettings?.enabled || !termsSettings.contentUrl) {
|
||||
|
@ -27,7 +28,7 @@ const TermsOfUse = ({ className }: Props) => {
|
|||
onChange={(checked) => {
|
||||
setTermsAgreement(checked);
|
||||
}}
|
||||
onTermsClick={isMobile ? termsOfUserModalHandler : undefined}
|
||||
onTermsClick={isMobile ? termsOfUseIframeModalHandler : undefined}
|
||||
/>
|
||||
<ModalContainer />
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
|
||||
import { modalPromisify } from '.';
|
||||
import TermsOfUseConfirmModal from '../TermsOfUseConfirmModal';
|
||||
|
||||
describe('modalPromisify', () => {
|
||||
const onResolve = jest.fn();
|
||||
const onReject = jest.fn();
|
||||
|
||||
it('render properly', () => {
|
||||
const PromisifyModal = modalPromisify(TermsOfUseConfirmModal);
|
||||
|
||||
const { getByText } = renderWithPageContext(
|
||||
<PromisifyModal isOpen instanceId="foo" onResolve={onResolve} onReject={onReject} />
|
||||
);
|
||||
|
||||
const confirmButton = getByText('action.agree');
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
expect(onResolve).toBeCalled();
|
||||
});
|
||||
|
||||
it('reject with message', () => {
|
||||
const PromisifyModal = modalPromisify(TermsOfUseConfirmModal);
|
||||
|
||||
const { getByText } = renderWithPageContext(
|
||||
<PromisifyModal isOpen instanceId="foo" onResolve={onResolve} onReject={onReject} />
|
||||
);
|
||||
|
||||
const cancelButton = getByText('action.cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(onReject).toBeCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { InstanceProps } from 'react-modal-promise';
|
||||
|
||||
import { TermsOfUseModalMessage } from '@/types';
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: (message?: TermsOfUseModalMessage) => void;
|
||||
};
|
||||
|
||||
export const modalPromisify =
|
||||
(ConfirmModal: (props: Props) => JSX.Element) =>
|
||||
({
|
||||
isOpen,
|
||||
onResolve,
|
||||
onReject,
|
||||
}: Omit<InstanceProps<boolean | TermsOfUseModalMessage>, 'open' | 'close'>) => {
|
||||
return (
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
onConfirm={() => {
|
||||
onResolve(true);
|
||||
}}
|
||||
onClose={(message?: TermsOfUseModalMessage) => {
|
||||
onReject(message ?? false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -4,12 +4,29 @@ import React from 'react';
|
|||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { signInBasic } from '@/apis/sign-in';
|
||||
import { termsOfUseConfirmModalPromise } from '@/containers/TermsOfUse/TermsOfUseConfirmModal';
|
||||
import { termsOfUseIframeModalPromise } from '@/containers/TermsOfUse/TermsOfUseIframeModal';
|
||||
import { TermsOfUseModalMessage } from '@/types';
|
||||
|
||||
import UsernameSignin from '.';
|
||||
|
||||
jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => Promise.resolve()) }));
|
||||
jest.mock('@/containers/TermsOfUse/TermsOfUseConfirmModal', () => ({
|
||||
termsOfUseConfirmModalPromise: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
jest.mock('@/containers/TermsOfUse/TermsOfUseIframeModal', () => ({
|
||||
termsOfUseIframeModalPromise: jest.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
const termsOfUseConfirmModalPromiseMock = termsOfUseConfirmModalPromise as jest.Mock;
|
||||
const termsOfUseIframeModalPromiseMock = termsOfUseIframeModalPromise as jest.Mock;
|
||||
|
||||
describe('<UsernameSignin>', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('render', () => {
|
||||
const { queryByText, container } = renderWithPageContext(<UsernameSignin />);
|
||||
expect(container.querySelector('input[name="username"]')).not.toBeNull();
|
||||
|
@ -52,6 +69,60 @@ describe('<UsernameSignin>', () => {
|
|||
expect(queryByText('required')).toBeNull();
|
||||
});
|
||||
|
||||
test('should show terms confirm modal', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameSignin />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
const usernameInput = container.querySelector('input[name="username"]');
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
if (usernameInput) {
|
||||
fireEvent.change(usernameInput, { target: { value: 'username' } });
|
||||
}
|
||||
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: 'password' } });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(signInBasic).toBeCalledWith('username', 'password', undefined);
|
||||
});
|
||||
|
||||
test('should show terms detail modal', async () => {
|
||||
termsOfUseConfirmModalPromiseMock.mockRejectedValue(TermsOfUseModalMessage.SHOW_DETAIL_MODAL);
|
||||
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
<UsernameSignin />
|
||||
</SettingsProvider>
|
||||
);
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
const usernameInput = container.querySelector('input[name="username"]');
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
if (usernameInput) {
|
||||
fireEvent.change(usernameInput, { target: { value: 'username' } });
|
||||
}
|
||||
|
||||
if (passwordInput) {
|
||||
fireEvent.change(passwordInput, { target: { value: 'password' } });
|
||||
}
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
expect(termsOfUseIframeModalPromiseMock).toBeCalledWith();
|
||||
});
|
||||
|
||||
test('submit form', async () => {
|
||||
const { getByText, container } = renderWithPageContext(
|
||||
<SettingsProvider>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useContext, useCallback } from 'react';
|
||||
|
||||
import { termsOfUseModalPromise } from '@/containers/TermsOfUse/TermsOfUsePromiseModal';
|
||||
import { termsOfUseConfirmModalPromise } from '@/containers/TermsOfUse/TermsOfUseConfirmModal';
|
||||
import { termsOfUseIframeModalPromise } from '@/containers/TermsOfUse/TermsOfUseIframeModal';
|
||||
import { TermsOfUseModalMessage } from '@/types';
|
||||
|
||||
import { PageContext } from './use-page-context';
|
||||
|
||||
|
@ -9,9 +11,9 @@ const useTerms = () => {
|
|||
|
||||
const { termsOfUse } = experienceSettings ?? {};
|
||||
|
||||
const termsOfUserModalHandler = useCallback(async () => {
|
||||
const termsOfUseIframeModalHandler = useCallback(async () => {
|
||||
try {
|
||||
await termsOfUseModalPromise();
|
||||
await termsOfUseIframeModalPromise();
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
|
@ -19,20 +21,37 @@ const useTerms = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const termsOfUseConfirmModalHandler = useCallback(async () => {
|
||||
try {
|
||||
await termsOfUseConfirmModalPromise();
|
||||
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
if (error === TermsOfUseModalMessage.SHOW_DETAIL_MODAL) {
|
||||
const result = await termsOfUseIframeModalHandler();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}, [termsOfUseIframeModalHandler]);
|
||||
|
||||
const termsValidation = useCallback(async () => {
|
||||
if (termsAgreement || !termsOfUse?.enabled || !termsOfUse.contentUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return termsOfUserModalHandler();
|
||||
}, [termsAgreement, termsOfUse, termsOfUserModalHandler]);
|
||||
return termsOfUseConfirmModalHandler();
|
||||
}, [termsAgreement, termsOfUse, termsOfUseConfirmModalHandler]);
|
||||
|
||||
return {
|
||||
termsSettings: termsOfUse,
|
||||
termsAgreement,
|
||||
termsValidation,
|
||||
setTermsAgreement,
|
||||
termsOfUserModalHandler,
|
||||
termsOfUseConfirmModalHandler,
|
||||
termsOfUseIframeModalHandler,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -30,3 +30,7 @@ export type SignInExperienceSettings = Omit<
|
|||
primarySignInMethod: SignInMethod;
|
||||
secondarySignInMethods: SignInMethod[];
|
||||
};
|
||||
|
||||
export enum TermsOfUseModalMessage {
|
||||
SHOW_DETAIL_MODAL = 'SHOW_DETAIL_MODAL',
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue