mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(ui): add mobile terms of use iframe modal (#947)
add mobile terms of use iframe modal
This commit is contained in:
parent
d8c8c041b9
commit
4abcda6820
13 changed files with 194 additions and 15 deletions
|
@ -42,6 +42,7 @@ const translation = {
|
|||
bind: 'Binding with {{address}}',
|
||||
back: 'Go Back',
|
||||
nav_back: 'Back',
|
||||
agree: 'Agree',
|
||||
},
|
||||
description: {
|
||||
email: 'email',
|
||||
|
|
|
@ -44,6 +44,7 @@ const translation = {
|
|||
bind: '绑定到 {{address}}',
|
||||
back: '返回',
|
||||
nav_back: '返回',
|
||||
agree: '同意',
|
||||
},
|
||||
description: {
|
||||
email: '邮箱',
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.overlay {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
top: 40px;
|
||||
bottom: 40px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: var(--color-dialogue);
|
||||
border-radius: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@include _.flex_column(stretch, center);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: _.unit(5);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--color-divider);
|
||||
@include _.flex_row;
|
||||
padding: _.unit(5);
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
> button:first-child {
|
||||
margin-right: _.unit(2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
|
||||
import * as modalStyles from '../../scss/modal.module.scss';
|
||||
import * as styles from './IframeConfirmModal.module.scss';
|
||||
import { ModalProps } from './type';
|
||||
|
||||
type Props = { url: string } & Omit<ModalProps, 'children'>;
|
||||
|
||||
const IframeConfirmModal = ({
|
||||
className,
|
||||
isOpen = false,
|
||||
url,
|
||||
cancelText = 'action.cancel',
|
||||
confirmText = 'action.confirm',
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
|
||||
return (
|
||||
<ReactModal
|
||||
role="dialog"
|
||||
isOpen={isOpen}
|
||||
className={classNames(styles.modal, className)}
|
||||
overlayClassName={classNames(modalStyles.overlay, styles.overlay)}
|
||||
appElement={document.querySelector('main') ?? undefined}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<iframe
|
||||
role="iframe"
|
||||
src={url}
|
||||
title="terms"
|
||||
frameBorder="0"
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Button type="secondary" onClick={onClose}>
|
||||
{t(cancelText)}
|
||||
</Button>
|
||||
<Button onClick={onConfirm ?? onClose}>{t(confirmText)}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ReactModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default IframeConfirmModal;
|
|
@ -1,2 +1,3 @@
|
|||
export { default as WebModal } from './AcModal';
|
||||
export { default as MobileModal } from './MobileModal';
|
||||
export { default as IframeModal } from './IframeConfirmModal';
|
||||
|
|
|
@ -13,9 +13,10 @@ type Props = {
|
|||
termsUrl: string;
|
||||
isChecked?: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
onTermsClick?: () => void;
|
||||
};
|
||||
|
||||
const TermsOfUse = ({ name, className, termsUrl, isChecked, onChange }: Props) => {
|
||||
const TermsOfUse = ({ name, className, termsUrl, isChecked, onChange, onTermsClick }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
|
||||
const prefix = t('description.agree_with_terms');
|
||||
|
@ -33,12 +34,13 @@ const TermsOfUse = ({ name, className, termsUrl, isChecked, onChange }: Props) =
|
|||
<TextLink
|
||||
className={styles.link}
|
||||
text="description.terms_of_use"
|
||||
href={termsUrl}
|
||||
href={onTermsClick ? undefined : termsUrl} // Do not open link if onTermsClick is provided
|
||||
target="_blank"
|
||||
type="secondary"
|
||||
onClick={(event) => {
|
||||
// Prevent above parent onClick event being triggered
|
||||
event.stopPropagation();
|
||||
onTermsClick?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
&.secondary {
|
||||
color: var(--color-caption);
|
||||
font: var(--font-body);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,12 @@ import React, { ReactNode } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import reactStringReplace from 'react-string-replace';
|
||||
|
||||
import { WebModal, MobileModal } from '@/components/ConfirmModal';
|
||||
import { WebModal as ConfirmModal } from '@/components/ConfirmModal';
|
||||
import TextLink from '@/components/TextLink';
|
||||
import usePlatform from '@/hooks/use-platform';
|
||||
|
||||
/**
|
||||
* For web use only confirm modal, does not contain Terms iframe
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
|
@ -15,8 +18,6 @@ type Props = {
|
|||
|
||||
const TermsOfUseConfirmModal = ({ isOpen = false, termsUrl, onConfirm, onClose }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { isMobile } = usePlatform();
|
||||
const ConfirmModal = isMobile ? MobileModal : WebModal;
|
||||
|
||||
const terms = t('description.terms_of_use');
|
||||
const content = t('description.agree_with_terms_modal', { terms });
|
||||
|
@ -26,7 +27,12 @@ const TermsOfUseConfirmModal = ({ isOpen = false, termsUrl, onConfirm, onClose }
|
|||
));
|
||||
|
||||
return (
|
||||
<ConfirmModal isOpen={isOpen} onConfirm={onConfirm} onClose={onClose}>
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
confirmText="action.agree"
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
>
|
||||
{modalContent}
|
||||
</ConfirmModal>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import TermsOfUseIframeModal from '.';
|
||||
|
||||
describe('TermsOfUseModal', () => {
|
||||
const onConfirm = jest.fn();
|
||||
const onCancel = jest.fn();
|
||||
|
||||
it('render properly', () => {
|
||||
const { queryByText } = render(
|
||||
<TermsOfUseIframeModal
|
||||
isOpen
|
||||
termsUrl="https://www.google.com/"
|
||||
onConfirm={onConfirm}
|
||||
onClose={onCancel}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(queryByText('action.agree')).not.toBeNull();
|
||||
|
||||
const iframe = screen.queryByRole('iframe');
|
||||
|
||||
expect(iframe).not.toBeNull();
|
||||
|
||||
if (iframe) {
|
||||
expect(iframe).toHaveProperty('src', 'https://www.google.com/');
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
|
||||
import { IframeModal } from '@/components/ConfirmModal';
|
||||
|
||||
/**
|
||||
* For mobile use only, includes embedded Terms iframe
|
||||
*/
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
termsUrl: string;
|
||||
};
|
||||
|
||||
const TermsOfUseIframeModal = ({ isOpen = false, termsUrl, onConfirm, onClose }: Props) => {
|
||||
return (
|
||||
<IframeModal
|
||||
isOpen={isOpen}
|
||||
confirmText="action.agree"
|
||||
url={termsUrl}
|
||||
onConfirm={onConfirm}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TermsOfUseIframeModal;
|
|
@ -2,15 +2,20 @@ 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 (
|
||||
<TermsOfUseConfirmModal
|
||||
<ConfirmModal
|
||||
isOpen={isOpen}
|
||||
termsUrl={termsOfUse?.contentUrl ?? ''}
|
||||
onConfirm={() => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import ModalContainer from 'react-modal-promise';
|
||||
|
||||
import TermsOfUseComponent from '@/components/TermsOfUse';
|
||||
import usePlatform from '@/hooks/use-platform';
|
||||
import useTerms from '@/hooks/use-terms';
|
||||
|
||||
type Props = {
|
||||
|
@ -9,7 +10,8 @@ type Props = {
|
|||
};
|
||||
|
||||
const TermsOfUse = ({ className }: Props) => {
|
||||
const { termsAgreement, setTermsAgreement, termsSettings } = useTerms();
|
||||
const { termsAgreement, setTermsAgreement, termsSettings, termsOfUserModalHandler } = useTerms();
|
||||
const { isMobile } = usePlatform();
|
||||
|
||||
if (!termsSettings?.enabled || !termsSettings.contentUrl) {
|
||||
return null;
|
||||
|
@ -25,6 +27,7 @@ const TermsOfUse = ({ className }: Props) => {
|
|||
onChange={(checked) => {
|
||||
setTermsAgreement(checked);
|
||||
}}
|
||||
onTermsClick={isMobile ? termsOfUserModalHandler : undefined}
|
||||
/>
|
||||
<ModalContainer />
|
||||
</>
|
||||
|
|
|
@ -9,11 +9,7 @@ const useTerms = () => {
|
|||
|
||||
const { termsOfUse } = experienceSettings ?? {};
|
||||
|
||||
const termsValidation = useCallback(async () => {
|
||||
if (termsAgreement || !termsOfUse?.enabled || !termsOfUse.contentUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const termsOfUserModalHandler = useCallback(async () => {
|
||||
try {
|
||||
await termsOfUseModalPromise();
|
||||
|
||||
|
@ -21,13 +17,22 @@ const useTerms = () => {
|
|||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, [termsAgreement, termsOfUse]);
|
||||
}, []);
|
||||
|
||||
const termsValidation = useCallback(async () => {
|
||||
if (termsAgreement || !termsOfUse?.enabled || !termsOfUse.contentUrl) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return termsOfUserModalHandler();
|
||||
}, [termsAgreement, termsOfUse, termsOfUserModalHandler]);
|
||||
|
||||
return {
|
||||
termsSettings: termsOfUse,
|
||||
termsAgreement,
|
||||
termsValidation,
|
||||
setTermsAgreement,
|
||||
termsOfUserModalHandler,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue