Skip to content

Commit

Permalink
[P2P]-feat: disclaimer modal for p2p users (#11046)
Browse files Browse the repository at this point in the history
* feat: disclaimer modal for p2p users

* fix: remove unused change

* fix: pr review comments

* fix: removed wrapper

* fix: changed interval logic to timeout

* fix: update test

* fix: prefix with p2p for the local storage key

* fix: added margin for modal content

* fix: remove unwanted change

* fix: button misaligned in ios

* fix: disclaimer interval set to 1 hour

* fix: change time to 1 hour for testing

* fix: changed interval duration back to 24 hours
  • Loading branch information
nada-deriv authored Nov 17, 2023
1 parent 4b50ab3 commit be53469
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 1 deletion.
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 @@ -612,6 +612,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
1 change: 1 addition & 0 deletions packages/components/stories/icon/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ export const icons =
'IcVerification',
'IcVisaDark',
'IcVisaLight',
'IcWarning',
'IcWebMoneyDark',
'IcWebMoneyLight',
'IcWebTerminal',
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}
/>,
<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}>
<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;
};

1 comment on commit be53469

@vercel
Copy link

@vercel vercel bot commented on be53469 Nov 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

deriv-app – ./

deriv-app.vercel.app
binary.sx
deriv-app-git-master.binary.sx
deriv-app.binary.sx

Please sign in to comment.