Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[P2P]-feat: disclaimer modal for p2p users #11046

Merged
merged 14 commits into from
Nov 17, 2023
Merged
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/components/src/components/icon/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,7 @@ import './common/ic-verification-success.svg';
import './common/ic-verification.svg';
import './common/ic-visa-dark.svg';
import './common/ic-visa-light.svg';
import './common/ic-warning.svg';
import './common/ic-web-money-dark.svg';
import './common/ic-web-money-light.svg';
import './common/ic-web-terminal.svg';
Expand Down
3 changes: 3 additions & 0 deletions packages/components/stories/icon/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ export const icons =
'IcPooSubmitted',
'IcPooVerified',
'IcPortfolio',
'IcPositionClosed',
'IcPreviewIcon',
'IcPreview',
'IcProfile',
Expand Down Expand Up @@ -613,6 +614,7 @@ export const icons =
'IcVerification',
'IcVisaDark',
'IcVisaLight',
'IcWarning',
'IcWebMoneyDark',
'IcWebMoneyLight',
'IcWebTerminal',
Expand Down Expand Up @@ -701,6 +703,7 @@ export const icons =
'IcFlagPt',
'IcFlagRu',
'IcFlagTh',
'IcFlagTr',
'IcFlagUk',
'IcFlagVi',
'IcFlagZhCn',
Expand Down
28 changes: 27 additions & 1 deletion packages/p2p/src/components/app-content.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,44 @@ import { useModalManagerContext } from 'Components/modal-manager/modal-manager-c
import TemporarilyBarredHint from 'Components/temporarily-barred-hint';
import { buy_sell } from 'Constants/buy-sell';
import { useStores } from 'Stores';

import { getHoursDifference } from 'Utils/date-time';
import { localize } from './i18next';

const INTERVAL_DURATION = 24; // 24 hours

const AppContent = ({ order_id }) => {
const { buy_sell_store, general_store } = useStores();
const { showModal, hideModal } = useModalManagerContext();
let timeout;
const {
notifications: { setP2POrderProps },
client: { loginid },
} = useStore();
const notification_count = useP2PNotificationCount();
const history = useHistory();

const handleDisclaimerTimeout = time_lapsed => {
timeout = setTimeout(() => {
showModal({ key: 'DisclaimerModal', props: { handleDisclaimerTimeout } });
// Display the disclaimer modal again after 24 hours
}, (INTERVAL_DURATION - time_lapsed) * 3600000);
};

React.useEffect(() => {
if (!general_store.should_show_dp2p_blocked) {
const time_lapsed = getHoursDifference(localStorage.getItem(`p2p_${loginid}_disclaimer_shown`));
if (time_lapsed === undefined || time_lapsed > INTERVAL_DURATION) {
showModal({ key: 'DisclaimerModal', props: { handleDisclaimerTimeout } });
} else {
handleDisclaimerTimeout(time_lapsed);
}
}

return () => {
clearTimeout(timeout);
};
}, []);

React.useEffect(() => {
buy_sell_store.setTableType(buy_sell.BUY);
return reaction(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { StoreProvider, mockStore } from '@deriv/stores';
import DisclaimerModal from '../disclaimer-modal';

const mock_modal_manager_context = {
hideModal: jest.fn(),
is_modal_open: true,
showModal: jest.fn(),
};

const mock = {
client: {
loginid: 'MX12345',
},
ui: {
is_mobile: false,
},
};

const handleDisclaimerTimeout = jest.fn();

jest.mock('Components/modal-manager/modal-manager-context', () => ({
useModalManagerContext: () => mock_modal_manager_context,
}));

describe('DisclaimerModal', () => {
let modal_root_el: HTMLElement;
beforeAll(() => {
modal_root_el = document.createElement('div');
modal_root_el.setAttribute('id', 'modal_root');
document.body.appendChild(modal_root_el);
Object.defineProperty(window, 'localStorage', {
value: {
setItem: jest.fn(),
},
});
});

afterAll(() => {
document.body.removeChild(modal_root_el);
});

const wrapper = ({ children }: { children: JSX.Element }) => (
<StoreProvider store={mockStore(mock)}>{children}</StoreProvider>
);

it('should render the modal with the correct title and content', () => {
render(<DisclaimerModal handleDisclaimerTimeout={handleDisclaimerTimeout} />, { wrapper });
expect(screen.getByText('For your safety:')).toBeInTheDocument();
});

it('should disable button when checkbox is not clicked', () => {
render(<DisclaimerModal handleDisclaimerTimeout={handleDisclaimerTimeout} />, { wrapper });
expect(screen.getByRole('button', { name: 'Confirm' })).toBeDisabled();
});

it('should enable button when checkbox is clicked', () => {
render(<DisclaimerModal handleDisclaimerTimeout={handleDisclaimerTimeout} />, { wrapper });
userEvent.click(screen.getByRole('checkbox', { name: 'I’ve read and understood the above reminder.' }));
expect(screen.getByRole('button', { name: 'Confirm' })).toBeEnabled();
});

it('should set value in local storage when confirm button is clicked', () => {
render(<DisclaimerModal handleDisclaimerTimeout={handleDisclaimerTimeout} />, { wrapper });
userEvent.click(screen.getByRole('checkbox', { name: 'I’ve read and understood the above reminder.' }));
userEvent.click(screen.getByRole('button', { name: 'Confirm' }));
expect(localStorage.setItem).toHaveBeenCalledWith('p2p_MX12345_disclaimer_shown', expect.any(String));
expect(handleDisclaimerTimeout).toHaveBeenCalledWith(0);
expect(mock_modal_manager_context.hideModal).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
.dc-modal {
&__container_disclaimer-modal {
.disclaimer-modal {
&__body {
padding: 0 2.4rem;

&-icon {
margin: 1.6rem auto;
}

&-list {
list-style-type: disc;
padding-left: 2rem;
margin-bottom: 1.6rem;
}

.dc-checkbox__box {
margin-left: 0;
}
}
}
.dc-modal-header {
&__title {
display: flex;
flex-direction: column;
padding-bottom: 0;
}
@include mobile {
height: 15rem;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';
import { Button, Checkbox, Icon, Modal, Text } from '@deriv/components';
import { useStore } from '@deriv/stores';
import { Localize } from 'Components/i18next';
import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context';

const ModalTitle = () => {
const { ui } = useStore();
const { is_mobile } = ui;
return (
<React.Fragment>
<Icon icon='IcWarning' size={64} className='disclaimer-modal__body-icon' />
<Text
as='p'
color='prominent'
weight='bold'
line_height={is_mobile ? 'xl' : 'xxl'}
size={is_mobile ? 'xs' : 's'}
>
<Localize i18n_default_text='For your safety:' />
</Text>
</React.Fragment>
);
};
const disclaimer_statements = [
<Localize
i18n_default_text='If you’re selling, only release funds to the buyer after you’ve received payment.'
key={0}
nada-deriv marked this conversation as resolved.
Show resolved Hide resolved
/>,
<Localize i18n_default_text='We’ll never ask you to release funds on behalf of anyone.' key={1} />,
<Localize
i18n_default_text="Read the instructions in the ad carefully before making your order. If there's anything unclear, check with the advertiser first."
key={2}
/>,
<Localize
i18n_default_text='Only discuss your P2P order details within the in-app chatbox, and nowhere else.'
key={3}
/>,
<Localize i18n_default_text='All P2P transactions are final and cannot be reversed.' key={4} />,
];

type TDisclaimerModalProps = {
handleDisclaimerTimeout: (time_left: number) => void;
};

const DisclaimerModal = ({ handleDisclaimerTimeout }: TDisclaimerModalProps) => {
const [is_checked, setIsChecked] = React.useState(false);
const { hideModal, is_modal_open } = useModalManagerContext();
const { client, ui } = useStore();
const { loginid } = client;
const { is_mobile } = ui;

const onClickConfirm = () => {
const current_date = new Date().toISOString();
localStorage.setItem(`p2p_${loginid}_disclaimer_shown`, current_date);
hideModal();
handleDisclaimerTimeout(0);
};

return (
<Modal
className='disclaimer-modal'
is_open={is_modal_open}
small
has_close_icon={false}
title={<ModalTitle />}
is_title_centered
>
<Modal.Body className='disclaimer-modal__body'>
<ul className='disclaimer-modal__body-list'>
{disclaimer_statements.map((statement, idx) => (
<li key={idx}>
nada-deriv marked this conversation as resolved.
Show resolved Hide resolved
<Text line_height={is_mobile ? 'l' : 'xl'} size={is_mobile ? 'xxs' : 'xs'}>
{statement}
</Text>
</li>
))}
</ul>
<Checkbox
onChange={() => setIsChecked(prev_state => !prev_state)}
name='disclaimer-checkbox'
value={is_checked}
label={
<Text line_height={is_mobile ? 'l' : 'xl'} size={is_mobile ? 'xxs' : 'xs'}>
<Localize i18n_default_text='I’ve read and understood the above reminder.' />
</Text>
}
/>
</Modal.Body>
<Modal.Footer>
<Button has_effect onClick={onClickConfirm} primary large disabled={!is_checked}>
<Localize i18n_default_text='Confirm' />
</Button>
</Modal.Footer>
</Modal>
);
};

export default DisclaimerModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import DisclaimerModal from './disclaimer-modal';
import './disclaimer-modal.scss';

export default DisclaimerModal;
3 changes: 3 additions & 0 deletions packages/p2p/src/constants/modals.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export const modals = {
/* webpackChunkName: "delete-payment-method-error-modal" */ 'Components/modal-manager/modals/delete-payment-method-error-modal'
)
),
DisclaimerModal: React.lazy(() =>
import(/* webpackChunkName: "disclaimer-modal" */ 'Components/modal-manager/modals/disclaimer-modal')
),
EditAdCancelModal: React.lazy(() =>
import(/* webpackChunkName: "edit-ad-cancel-modal" */ 'Components/modal-manager/modals/edit-ad-cancel-modal')
),
Expand Down
15 changes: 15 additions & 0 deletions packages/p2p/src/utils/date-time.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,18 @@ export const secondsToTimer = distance => {

return `${toDoubleDigits(hours)}:${toDoubleDigits(minutes)}:${toDoubleDigits(seconds)}`;
};

/**
* Calculate the difference in hours between the current time and a given time.
*
* @param {string} time_set - The time to compare against.
* @returns {number|undefined} The difference in hours between the current time and the given time. Returns `undefined` if the input is invalid.
*/
export const getHoursDifference = time_set => {
if (!time_set) return undefined;
const current_time = new Date();
const updated_time = new Date(time_set);
const difference = current_time.getTime() - updated_time.getTime();
const hours_difference = Math.floor(difference / 1000 / 60 / 60);
return hours_difference;
};