0
Fork 0
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:
simeng-li 2022-05-25 16:42:18 +08:00 committed by GitHub
parent d8c8c041b9
commit 4abcda6820
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 194 additions and 15 deletions

View file

@ -42,6 +42,7 @@ const translation = {
bind: 'Binding with {{address}}',
back: 'Go Back',
nav_back: 'Back',
agree: 'Agree',
},
description: {
email: 'email',

View file

@ -44,6 +44,7 @@ const translation = {
bind: '绑定到 {{address}}',
back: '返回',
nav_back: '返回',
agree: '同意',
},
description: {
email: '邮箱',

View file

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

View file

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

View file

@ -1,2 +1,3 @@
export { default as WebModal } from './AcModal';
export { default as MobileModal } from './MobileModal';
export { default as IframeModal } from './IframeConfirmModal';

View file

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

View file

@ -14,6 +14,7 @@
&.secondary {
color: var(--color-caption);
font: var(--font-body);
text-decoration: underline;
}
}

View file

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

View file

@ -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/');
}
});
});

View file

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

View file

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

View file

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

View file

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