From b142e79b0afc9bf18fe89804ed5b22e90a295a1d Mon Sep 17 00:00:00 2001 From: Carol Sachdeva <58209918+carol-binary@users.noreply.github.com> Date: Thu, 8 Sep 2022 18:11:13 +0800 Subject: [PATCH] P2p 2fa feature (#6422) * carol/ P2P: 2FA (#6009) * add: icons * add: one more icon * add email verification modal * add invalid verification link modal * add email verified * add email blocked modal * add email blocked modal * use align prop instead * carol/ P2P: Email verification for orders (#6299) * fix: quotes * add: email verification * cleanup * don't kill me * add response checks * add comment * add: amount + currency * fixed loading of order details and chat * fixed design on responsive * fixed verification modal in responsive * show modal if error * fix * fixed truncated modal in ios * fix time * fix invalid verification modal * fix * fix * fixed verification modal in desktop * fix responsive +logged out user * fix: design for seller * the solution to all my problems * fix: modal * fix logout + modal * hide extra modal * i got 99 problems and 2fa is all of em * fix * fix * fix: amount * fix: amount * fix: add modal * fix: rating modal Co-authored-by: Farrah Mae Ochoa Co-authored-by: Nijil Nirmal Co-authored-by: Farrah Mae Ochoa --- .../src/pages/p2p-cashier/p2p-cashier.jsx | 44 ++- .../icon/common/ic-email-sent-p2p.svg | 1 + .../ic-email-verification-link-blocked.svg | 1 + .../ic-email-verification-link-invalid.svg | 1 + .../ic-email-verification-link-valid.svg | 1 + .../components/src/components/icon/icons.js | 4 + packages/components/stories/icon/icons.js | 4 + .../src/App/Containers/Redirect/redirect.jsx | 8 + packages/p2p/jest.config.js | 2 +- packages/p2p/src/components/app.jsx | 43 ++- .../components/buy-sell/buy-sell-modal.jsx | 2 +- .../email-link-blocked-modal.jsx | 42 +++ .../email-link-blocked-modal.scss | 9 + .../email-link-blocked-modal/index.js | 4 + .../email-link-verified-modal.jsx | 57 ++++ .../email-link-verified-modal.scss | 13 + .../email-link-verified-modal/index.js | 4 + .../email-verification-modal.jsx | 109 +++++++ .../email-verification-modal.scss | 42 +++ .../email-verification-modal/index.js | 4 + .../components/error-modal/error-modal.jsx | 24 ++ .../p2p/src/components/error-modal/index.js | 3 + .../invalid-verification-link-modal/index.js | 4 + .../invalid-verification-link-modal.jsx | 56 ++++ .../invalid-verification-link-modal.scss | 13 + .../p2p/src/components/loading-modal/index.js | 3 + .../loading-modal/loading-modal.jsx | 17 ++ .../order-details-cancel-modal.jsx | 2 +- .../order-details-confirm-modal.jsx | 10 +- .../order-details/order-details-footer.jsx | 9 + .../order-details/order-details-wrapper.jsx | 7 +- .../order-details/order-details.jsx | 46 ++- packages/p2p/src/components/orders/orders.jsx | 7 +- .../components/rating-modal/rating-modal.jsx | 3 +- packages/p2p/src/stores/order-store.js | 287 +++++++++++++----- 35 files changed, 769 insertions(+), 117 deletions(-) create mode 100644 packages/components/src/components/icon/common/ic-email-sent-p2p.svg create mode 100644 packages/components/src/components/icon/common/ic-email-verification-link-blocked.svg create mode 100644 packages/components/src/components/icon/common/ic-email-verification-link-invalid.svg create mode 100644 packages/components/src/components/icon/common/ic-email-verification-link-valid.svg create mode 100644 packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.jsx create mode 100644 packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.scss create mode 100644 packages/p2p/src/components/email-link-blocked-modal/index.js create mode 100644 packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.jsx create mode 100644 packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.scss create mode 100644 packages/p2p/src/components/email-link-verified-modal/index.js create mode 100644 packages/p2p/src/components/email-verification-modal/email-verification-modal.jsx create mode 100644 packages/p2p/src/components/email-verification-modal/email-verification-modal.scss create mode 100644 packages/p2p/src/components/email-verification-modal/index.js create mode 100644 packages/p2p/src/components/error-modal/error-modal.jsx create mode 100644 packages/p2p/src/components/error-modal/index.js create mode 100644 packages/p2p/src/components/invalid-verification-link-modal/index.js create mode 100644 packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx create mode 100644 packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.scss create mode 100644 packages/p2p/src/components/loading-modal/index.js create mode 100644 packages/p2p/src/components/loading-modal/loading-modal.jsx diff --git a/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx b/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx index 0bb43fdd7bfe..dc584d670e05 100644 --- a/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx +++ b/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx @@ -34,6 +34,9 @@ const P2PCashier = ({ setOnRemount, }) => { const [order_id, setOrderId] = React.useState(null); + const [action_param, setActionParam] = React.useState(); + const [code_param, setCodeParam] = React.useState(); + const server_time = { get, init, @@ -42,21 +45,46 @@ const P2PCashier = ({ React.useEffect(() => { const url_params = new URLSearchParams(location.search); - const passed_order_id = url_params.get('order'); + let passed_order_id; + + setActionParam(url_params.get('action')); + if (is_mobile) { + setCodeParam(localStorage.getItem('verification_code.p2p_order_confirm')); + } else if (!code_param) { + if (url_params.has('code')) { + setCodeParam(url_params.get('code')); + } else if (localStorage.getItem('verification_code.p2p_order_confirm')) { + setCodeParam(localStorage.getItem('verification_code.p2p_order_confirm')); + } + } + + // Different emails give us different params (order / order_id), + // don't remove order_id since it's consistent for mobile and web for 2FA + if (url_params.has('order_id')) { + passed_order_id = url_params.get('order_id'); + } else if (url_params.has('order')) { + passed_order_id = url_params.get('order'); + } if (passed_order_id) { setQueryOrder(passed_order_id); } return () => setQueryOrder(null); - }, [location.search, setQueryOrder]); + }, [setQueryOrder]); const setQueryOrder = React.useCallback( input_order_id => { const current_query_params = new URLSearchParams(location.search); - if (current_query_params.has('order')) { + if (is_mobile) { + current_query_params.delete('action'); + current_query_params.delete('code'); + } + + if (current_query_params.has('order_id') || current_query_params.has('order')) { current_query_params.delete('order'); + current_query_params.delete('order_id'); } if (input_order_id) { @@ -91,8 +119,9 @@ const P2PCashier = ({ return ( ); }; diff --git a/packages/components/src/components/icon/common/ic-email-sent-p2p.svg b/packages/components/src/components/icon/common/ic-email-sent-p2p.svg new file mode 100644 index 000000000000..ac6603322838 --- /dev/null +++ b/packages/components/src/components/icon/common/ic-email-sent-p2p.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/icon/common/ic-email-verification-link-blocked.svg b/packages/components/src/components/icon/common/ic-email-verification-link-blocked.svg new file mode 100644 index 000000000000..6da1da0ce86a --- /dev/null +++ b/packages/components/src/components/icon/common/ic-email-verification-link-blocked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/icon/common/ic-email-verification-link-invalid.svg b/packages/components/src/components/icon/common/ic-email-verification-link-invalid.svg new file mode 100644 index 000000000000..6edf85f052d5 --- /dev/null +++ b/packages/components/src/components/icon/common/ic-email-verification-link-invalid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/icon/common/ic-email-verification-link-valid.svg b/packages/components/src/components/icon/common/ic-email-verification-link-valid.svg new file mode 100644 index 000000000000..c7f5bcb37d46 --- /dev/null +++ b/packages/components/src/components/icon/common/ic-email-verification-link-valid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/components/icon/icons.js b/packages/components/src/components/icon/icons.js index efecd9081e74..14eb0e3b79ac 100644 --- a/packages/components/src/components/icon/icons.js +++ b/packages/components/src/components/icon/icons.js @@ -238,9 +238,13 @@ import './common/ic-edit.svg'; import './common/ic-email-firewall.svg'; import './common/ic-email-outline.svg'; import './common/ic-email-sent-dashboard.svg'; +import './common/ic-email-sent-p2p.svg'; import './common/ic-email-sent.svg'; import './common/ic-email-spam.svg'; import './common/ic-email-typo.svg'; +import './common/ic-email-verification-link-blocked.svg'; +import './common/ic-email-verification-link-invalid.svg'; +import './common/ic-email-verification-link-valid.svg'; import './common/ic-email-verified.svg'; import './common/ic-email.svg'; import './common/ic-empty-folder.svg'; diff --git a/packages/components/stories/icon/icons.js b/packages/components/stories/icon/icons.js index d6a707475f3f..20071acf72d1 100644 --- a/packages/components/stories/icon/icons.js +++ b/packages/components/stories/icon/icons.js @@ -245,9 +245,13 @@ export const icons = 'IcEmailFirewall', 'IcEmailOutline', 'IcEmailSentDashboard', + 'IcEmailSentP2p', 'IcEmailSent', 'IcEmailSpam', 'IcEmailTypo', + 'IcEmailVerificationLinkBlocked', + 'IcEmailVerificationLinkInvalid', + 'IcEmailVerificationLinkValid', 'IcEmailVerified', 'IcEmail', 'IcEmptyFolder', diff --git a/packages/core/src/App/Containers/Redirect/redirect.jsx b/packages/core/src/App/Containers/Redirect/redirect.jsx index e390229392d1..39e693242409 100644 --- a/packages/core/src/App/Containers/Redirect/redirect.jsx +++ b/packages/core/src/App/Containers/Redirect/redirect.jsx @@ -145,6 +145,14 @@ const Redirect = ({ redirected_to_route = true; break; } + case 'p2p_order_confirm': { + history.push({ + pathname: routes.cashier_p2p, + search: url_query_string, + }); + redirected_to_route = true; + break; + } default: break; diff --git a/packages/p2p/jest.config.js b/packages/p2p/jest.config.js index 4d9bcdb4075f..6d193b80edd3 100644 --- a/packages/p2p/jest.config.js +++ b/packages/p2p/jest.config.js @@ -20,7 +20,7 @@ module.exports = { '/crowdin/', // TODO: Update the test files once the major features are done // This is a temporary change, I hope - '/src/components/order-details/', + '/src/components/order*', ], coveragePathIgnorePatterns: [ '/.eslintrc.js', diff --git a/packages/p2p/src/components/app.jsx b/packages/p2p/src/components/app.jsx index acbf5ca0eee0..c484bd41e3cc 100644 --- a/packages/p2p/src/components/app.jsx +++ b/packages/p2p/src/components/app.jsx @@ -12,8 +12,19 @@ import './app.scss'; const App = props => { const { general_store, order_store } = useStores(); - const { balance, className, history, lang, Notifications, order_id, server_time, websocket_api, setOnRemount } = - props; + const { + balance, + className, + history, + lang, + Notifications, + order_id, + server_time, + verification_action, + verification_code, + websocket_api, + setOnRemount, + } = props; React.useEffect(() => { general_store.setAppProps(props); @@ -68,6 +79,20 @@ const App = props => { setLanguage(lang); }, [lang]); + React.useEffect(() => { + if (verification_code) { + // We need an extra state since we delete the code from the query params. + // Do not remove. + order_store.setVerificationCode(verification_code); + } + if (verification_action && verification_code) { + order_store.setIsLoadingModalOpen(true); + order_store.verifyEmailVerificationCode(verification_action, verification_code); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [verification_action, verification_code]); + return (
@@ -77,25 +102,17 @@ const App = props => { }; App.propTypes = { + balance: PropTypes.string, className: PropTypes.string, - client: PropTypes.shape({ - currency: PropTypes.string.isRequired, - is_virtual: PropTypes.bool.isRequired, - local_currency_config: PropTypes.shape({ - currency: PropTypes.string.isRequired, - decimal_places: PropTypes.number, - }).isRequired, - loginid: PropTypes.string.isRequired, - residence: PropTypes.string.isRequired, - }), history: PropTypes.object, - balance: PropTypes.string, lang: PropTypes.string, modal_root_id: PropTypes.string.isRequired, order_id: PropTypes.string, server_time: PropTypes.object, setNotificationCount: PropTypes.func, setOnRemount: PropTypes.func, + verification_action: PropTypes.string, + verification_code: PropTypes.string, websocket_api: PropTypes.object.isRequired, }; diff --git a/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx b/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx index d45ebcc90fde..e27d8f0f7e68 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx @@ -135,8 +135,8 @@ const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldSho }; const onConfirmClick = order_info => { - order_store.setOrderId(order_info.id); general_store.redirectTo('orders', { nav: { location: 'buy_sell' } }); + order_store.setOrderId(order_info.id); setShouldShowPopup(false); buy_sell_store.setShowAdvertiserPage(false); }; diff --git a/packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.jsx b/packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.jsx new file mode 100644 index 000000000000..249b4887c8a9 --- /dev/null +++ b/packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icon, Modal, Text } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +const EmailLinkBlockedModal = ({ + // TODO: Uncomment when time is available in BE response + // blocked_for_minutes, + email_link_blocked_modal_error_message, + is_email_link_blocked_modal_open, + setIsEmailLinkBlockedModalOpen, +}) => { + return ( + <>} + toggleModal={() => setIsEmailLinkBlockedModalOpen(false)} + width='440px' + > + + + + + + + {email_link_blocked_modal_error_message} + + + + ); +}; + +EmailLinkBlockedModal.propTypes = { + // TODO: Uncomment when time is available in BE response + // blocked_for_minutes: PropTypes.number, + email_link_blocked_modal_error_message: PropTypes.string, + is_email_link_blocked_modal_open: PropTypes.bool, + setIsEmailLinkBlockedModalOpen: PropTypes.func, +}; + +export default EmailLinkBlockedModal; diff --git a/packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.scss b/packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.scss new file mode 100644 index 000000000000..975c829f0532 --- /dev/null +++ b/packages/p2p/src/components/email-link-blocked-modal/email-link-blocked-modal.scss @@ -0,0 +1,9 @@ +.email-link-blocked-modal { + align-items: center; + display: flex; + flex-direction: column; + + &--text { + margin: 2.4rem 0; + } +} diff --git a/packages/p2p/src/components/email-link-blocked-modal/index.js b/packages/p2p/src/components/email-link-blocked-modal/index.js new file mode 100644 index 000000000000..356741ec4cc6 --- /dev/null +++ b/packages/p2p/src/components/email-link-blocked-modal/index.js @@ -0,0 +1,4 @@ +import EmailLinkBlockedModal from './email-link-blocked-modal.jsx'; +import './email-link-blocked-modal.scss'; + +export default EmailLinkBlockedModal; diff --git a/packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.jsx b/packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.jsx new file mode 100644 index 000000000000..cd4cdb8d9656 --- /dev/null +++ b/packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Icon, Modal, Text } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +const EmailLinkVerifiedModal = ({ + amount, + currency, + is_email_link_verified_modal_open, + onClickConfirm, + setIsEmailLinkVerifiedModalOpen, +}) => { + return ( + <>} + toggleModal={() => setIsEmailLinkVerifiedModalOpen(false)} + width='440px' + > + + + + + + + + + + + + + + ); +}; + +EmailLinkVerifiedModal.propTypes = { + amount: PropTypes.string, + currency: PropTypes.string, + is_email_link_verified_modal_open: PropTypes.bool, + onClickConfirm: PropTypes.func, + setIsEmailLinkVerifiedModalOpen: PropTypes.func, +}; + +export default EmailLinkVerifiedModal; diff --git a/packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.scss b/packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.scss new file mode 100644 index 000000000000..e5b0c2fbb694 --- /dev/null +++ b/packages/p2p/src/components/email-link-verified-modal/email-link-verified-modal.scss @@ -0,0 +1,13 @@ +.email-verified-modal { + align-items: center; + display: flex; + flex-direction: column; + + &--footer { + align-self: center; + } + + &--text { + margin: 2.4rem 0; + } +} diff --git a/packages/p2p/src/components/email-link-verified-modal/index.js b/packages/p2p/src/components/email-link-verified-modal/index.js new file mode 100644 index 000000000000..35446e1c3a44 --- /dev/null +++ b/packages/p2p/src/components/email-link-verified-modal/index.js @@ -0,0 +1,4 @@ +import EmailLinkVerifiedModal from './email-link-verified-modal.jsx'; +import './email-link-verified-modal.scss'; + +export default EmailLinkVerifiedModal; diff --git a/packages/p2p/src/components/email-verification-modal/email-verification-modal.jsx b/packages/p2p/src/components/email-verification-modal/email-verification-modal.jsx new file mode 100644 index 000000000000..b17d9a663e86 --- /dev/null +++ b/packages/p2p/src/components/email-verification-modal/email-verification-modal.jsx @@ -0,0 +1,109 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Icon, Modal, Text } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +const EmailVerificationModal = ({ + email_address, + is_email_verification_modal_open, + onClickResendEmailButton, + setIsEmailVerificationModalOpen, + should_show_resend_email_button = true, + // TODO: Uncomment when time is available in BE response + // remaining_time, + // verification_link_expiry_time, +}) => { + const [should_show_reasons_if_no_email, setShouldShowReasonsIfNoEmail] = React.useState(false); + + return ( + <>} + toggleModal={() => setIsEmailVerificationModalOpen(false)} + width='440px' + > + + + + ]} + values={{ email_address }} + /> + + + {/* TODO: Uncomment when time is available in BE response */} + + + setShouldShowReasonsIfNoEmail(true)} + size='xs' + weight='bold' + > + + + {should_show_reasons_if_no_email && ( + +
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + +
+
+ )} +
+ {should_show_resend_email_button && should_show_reasons_if_no_email && ( + + + + )} +
+ ); +}; + +EmailVerificationModal.propTypes = { + email_address: PropTypes.string, + is_email_verification_modal_open: PropTypes.bool, + onClickResendEmailButton: PropTypes.func, + // TODO: Uncomment when time is available in BE response + // remaining_time: PropTypes.string, + setIsEmailVerificationModalOpen: PropTypes.func, + should_show_resend_email_button: PropTypes.bool, + // TODO: Uncomment when time is available in BE response + // verification_link_expiry_time: PropTypes.number, +}; + +export default EmailVerificationModal; diff --git a/packages/p2p/src/components/email-verification-modal/email-verification-modal.scss b/packages/p2p/src/components/email-verification-modal/email-verification-modal.scss new file mode 100644 index 000000000000..ad8d834fa79b --- /dev/null +++ b/packages/p2p/src/components/email-verification-modal/email-verification-modal.scss @@ -0,0 +1,42 @@ +.dc-modal__container_email-verification-modal { + max-height: 80vh; + overflow: auto; +} + +.email-verification-modal { + &--body { + align-items: center; + display: flex; + flex-direction: column; + } + + &--email_text { + margin: 2.4rem 0; + } + + &--footer { + @include mobile { + justify-content: center; + } + } + + &--reason { + display: flex; + flex-direction: row; + gap: 1.6rem; + margin: 2.4rem 0; + + &__text { + max-width: 34rem; + } + } + + &--receive_email_text { + cursor: pointer; + margin: 3rem 0 0.6rem; + + @include mobile { + margin: 3rem 0 2.6rem; + } + } +} diff --git a/packages/p2p/src/components/email-verification-modal/index.js b/packages/p2p/src/components/email-verification-modal/index.js new file mode 100644 index 000000000000..6101a5043db1 --- /dev/null +++ b/packages/p2p/src/components/email-verification-modal/index.js @@ -0,0 +1,4 @@ +import EmailVerificationModal from './email-verification-modal.jsx'; +import './email-verification-modal.scss'; + +export default EmailVerificationModal; diff --git a/packages/p2p/src/components/error-modal/error-modal.jsx b/packages/p2p/src/components/error-modal/error-modal.jsx new file mode 100644 index 000000000000..05a356c5d7cc --- /dev/null +++ b/packages/p2p/src/components/error-modal/error-modal.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +const ErrorModal = ({ error_message, error_modal_title, is_error_modal_open, setIsErrorModalOpen }) => { + return ( + + {error_message} + + + + + ); +}; + +ErrorModal.propTypes = { + error_message: PropTypes.string, + error_modal_title: PropTypes.string, + is_error_modal_open: PropTypes.bool, + setIsErrorModalOpen: PropTypes.func, +}; diff --git a/packages/p2p/src/components/error-modal/index.js b/packages/p2p/src/components/error-modal/index.js new file mode 100644 index 000000000000..1e38a565cb4c --- /dev/null +++ b/packages/p2p/src/components/error-modal/index.js @@ -0,0 +1,3 @@ +import ErrorModal from './error-modal.jsx'; + +export default ErrorModal; diff --git a/packages/p2p/src/components/invalid-verification-link-modal/index.js b/packages/p2p/src/components/invalid-verification-link-modal/index.js new file mode 100644 index 000000000000..bf5d817dfe0e --- /dev/null +++ b/packages/p2p/src/components/invalid-verification-link-modal/index.js @@ -0,0 +1,4 @@ +import InvalidVerificationLinkModal from './invalid-verification-link-modal.jsx'; +import './invalid-verification-link-modal.scss'; + +export default InvalidVerificationLinkModal; diff --git a/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx b/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx new file mode 100644 index 000000000000..640d18aef1a2 --- /dev/null +++ b/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Icon, Modal, Text } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +const InvalidVerificationLinkModal = ({ + invalid_verification_link_error_message, + is_invalid_verification_link_modal_open, + onClickGetNewLinkButton, + setIsInvalidVerificationLinkModalOpen, + // TODO: Uncomment when time is available in BE response + // verification_link_expiry_time, +}) => { + return ( + <>} + toggleModal={() => setIsInvalidVerificationLinkModalOpen(false)} + width='440px' + > + + + + + + + {invalid_verification_link_error_message} + + + + + + + ); +}; + +InvalidVerificationLinkModal.propTypes = { + invalid_verification_link_error_message: PropTypes.string, + is_invalid_verification_link_modal_open: PropTypes.bool, + onClickGetNewLinkButton: PropTypes.func, + setIsInvalidVerificationLinkModalOpen: PropTypes.func, + // TODO: Uncomment when time is available in BE response + // verification_link_expiry_time: PropTypes.number, +}; + +export default InvalidVerificationLinkModal; diff --git a/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.scss b/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.scss new file mode 100644 index 000000000000..887366940e0f --- /dev/null +++ b/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.scss @@ -0,0 +1,13 @@ +.invalid-verification-link-modal { + align-items: center; + display: flex; + flex-direction: column; + + &--footer { + align-self: center; + } + + &--text { + margin: 2.4rem 0; + } +} diff --git a/packages/p2p/src/components/loading-modal/index.js b/packages/p2p/src/components/loading-modal/index.js new file mode 100644 index 000000000000..41606f6895e6 --- /dev/null +++ b/packages/p2p/src/components/loading-modal/index.js @@ -0,0 +1,3 @@ +import LoadingModal from './loading-modal.jsx'; + +export default LoadingModal; diff --git a/packages/p2p/src/components/loading-modal/loading-modal.jsx b/packages/p2p/src/components/loading-modal/loading-modal.jsx new file mode 100644 index 000000000000..7ff444d16f41 --- /dev/null +++ b/packages/p2p/src/components/loading-modal/loading-modal.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Loading, Modal } from '@deriv/components'; + +const LoadingModal = ({ is_loading_modal_open }) => { + return ( + + + + ); +}; + +LoadingModal.propTypes = { + is_loading_modal_open: PropTypes.bool, +}; + +export default LoadingModal; diff --git a/packages/p2p/src/components/order-details/order-details-cancel-modal.jsx b/packages/p2p/src/components/order-details/order-details-cancel-modal.jsx index dde71eedf3bd..e327158eff91 100644 --- a/packages/p2p/src/components/order-details/order-details-cancel-modal.jsx +++ b/packages/p2p/src/components/order-details/order-details-cancel-modal.jsx @@ -56,7 +56,7 @@ const OrderDetailsCancelModal = ({ hideCancelOrderModal, order_id, should_show_c ) : ( { hideConfirmOrderModal(); - order_store.confirmOrderRequest(id); - if (!is_buy_order_for_user) { - clearTimeout(wait); - - const wait = setTimeout(() => { - order_store.setIsRatingModalOpen(true); - }, 250); - } + setIsCheckboxChecked(false); + order_store.confirmOrderRequest(id, is_buy_order_for_user); }} > {is_buy_order_for_user ? ( diff --git a/packages/p2p/src/components/order-details/order-details-footer.jsx b/packages/p2p/src/components/order-details/order-details-footer.jsx index 4e83d97faf40..7885ba6090aa 100644 --- a/packages/p2p/src/components/order-details/order-details-footer.jsx +++ b/packages/p2p/src/components/order-details/order-details-footer.jsx @@ -11,6 +11,7 @@ import OrderDetailsConfirmModal from './order-details-confirm-modal.jsx'; const OrderDetailsFooter = observer(() => { const { order_store } = useStores(); const { + // id, is_buy_order_for_user, should_show_cancel_and_paid_button, should_show_complain_and_received_button, @@ -42,6 +43,14 @@ const OrderDetailsFooter = observer(() => { const hideConfirmOrderModal = () => setShouldShowConfirmModal(false); const showConfirmOrderModal = () => setShouldShowConfirmModal(true); + // TODO: Uncomment this when we're ready to remove the modal + // const showConfirmOrderModal = () => { + // if (is_buy_order_for_user) { + // setShouldShowConfirmModal(true); + // } else { + // order_store.confirmOrderRequest(id); + // } + // }; if (should_show_cancel_and_paid_button) { return ( diff --git a/packages/p2p/src/components/order-details/order-details-wrapper.jsx b/packages/p2p/src/components/order-details/order-details-wrapper.jsx index 8f43b341e0ab..e08e8fea5f53 100644 --- a/packages/p2p/src/components/order-details/order-details-wrapper.jsx +++ b/packages/p2p/src/components/order-details/order-details-wrapper.jsx @@ -6,7 +6,7 @@ import PageReturn from 'Components/page-return/page-return.jsx'; import { useStores } from 'Stores'; import OrderDetailsFooter from 'Components/order-details/order-details-footer.jsx'; -const OrderDetailsWrapper = ({ children, onPageReturn, page_title }) => { +const OrderDetailsWrapper = ({ children, page_title }) => { const { order_store, sendbird_store } = useStores(); return isMobile() ? (
@@ -16,7 +16,7 @@ const OrderDetailsWrapper = ({ children, onPageReturn, page_title }) => { height_offset='80px' is_flex is_modal_open - pageHeaderReturnFn={onPageReturn} + pageHeaderReturnFn={order_store.onPageReturn} page_header_text={page_title} renderPageHeaderTrailingIcon={() => ( {
) : ( - + {children} ); @@ -46,7 +46,6 @@ const OrderDetailsWrapper = ({ children, onPageReturn, page_title }) => { OrderDetailsWrapper.propTypes = { children: PropTypes.any, - onPageReturn: PropTypes.func, page_title: PropTypes.string, }; diff --git a/packages/p2p/src/components/order-details/order-details.jsx b/packages/p2p/src/components/order-details/order-details.jsx index d0421171477c..24b4be6c0d19 100644 --- a/packages/p2p/src/components/order-details/order-details.jsx +++ b/packages/p2p/src/components/order-details/order-details.jsx @@ -1,11 +1,11 @@ import classNames from 'classnames'; import React from 'react'; -import PropTypes from 'prop-types'; import { Button, HintBox, Icon, Text, ThemedScrollbars } from '@deriv/components'; import { formatMoney, isDesktop, isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; import { Localize, localize } from 'Components/i18next'; import Chat from 'Components/orders/chat/chat.jsx'; +import EmailVerificationModal from 'Components/email-verification-modal'; import RatingModal from 'Components/rating-modal'; import StarRating from 'Components/star-rating'; import UserRatingButton from 'Components/user-rating-button'; @@ -20,9 +20,13 @@ import PaymentMethodAccordionContent from './payment-method-accordion-content.js import MyProfileSeparatorContainer from '../my-profile/my-profile-separator-container'; import { setDecimalPlaces, removeTrailingZeros, roundOffDecimal } from 'Utils/format-value'; import 'Components/order-details/order-details.scss'; +import LoadingModal from '../loading-modal'; +import InvalidVerificationLinkModal from '../invalid-verification-link-modal'; +import EmailLinkBlockedModal from '../email-link-blocked-modal'; +import EmailLinkVerifiedModal from '../email-link-verified-modal'; import { getDateAfterHours } from 'Utils/date-time'; -const OrderDetails = observer(({ onPageReturn }) => { +const OrderDetails = observer(() => { const { general_store, order_store, sendbird_store } = useStores(); const { @@ -72,6 +76,8 @@ const OrderDetails = observer(({ onPageReturn }) => { const disposeListeners = sendbird_store.registerEventListeners(); const disposeReactions = sendbird_store.registerMobXReactions(); + order_store.getSettings(); + order_store.getWebsiteStatus(); order_store.setRatingValue(0); order_store.setIsRecommended(undefined); @@ -85,6 +91,7 @@ const OrderDetails = observer(({ onPageReturn }) => { disposeListeners(); disposeReactions(); order_store.setOrderPaymentMethodDetails(undefined); + order_store.setOrderId(null); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -110,7 +117,7 @@ const OrderDetails = observer(({ onPageReturn }) => { : client_details?.is_recommended; return ( - + {is_active_order && ( { /> )} + {!is_buy_order_for_user && ( + + order_store.confirmOrderRequest(id)} + setIsEmailVerificationModalOpen={order_store.setIsEmailVerificationModalOpen} + /> + order_store.confirmOrder(is_buy_order_for_user)} + setIsEmailLinkVerifiedModalOpen={order_store.setIsEmailLinkVerifiedModalOpen} + /> + order_store.confirmOrderRequest(id)} + /> + + + + )}
@@ -395,8 +431,4 @@ const OrderDetails = observer(({ onPageReturn }) => { ); }); -OrderDetails.propTypes = { - onPageReturn: PropTypes.func, -}; - export default OrderDetails; diff --git a/packages/p2p/src/components/orders/orders.jsx b/packages/p2p/src/components/orders/orders.jsx index c0899b893a6f..dcd56b102159 100644 --- a/packages/p2p/src/components/orders/orders.jsx +++ b/packages/p2p/src/components/orders/orders.jsx @@ -19,7 +19,10 @@ const Orders = observer(() => { React.useEffect(() => { const disposeOrderIdReaction = reaction( () => order_store.order_id, - () => order_store.onOrderIdUpdate(), + () => { + // DO NOT REMOVE. This fixes all P2P order routing issues + order_store.onOrderIdUpdate(); + }, { fireImmediately: true } ); @@ -41,7 +44,7 @@ const Orders = observer(() => { if (order_store.order_information) { return (
- order_store.hideDetails(true)} /> +
); } diff --git a/packages/p2p/src/components/rating-modal/rating-modal.jsx b/packages/p2p/src/components/rating-modal/rating-modal.jsx index c63bc2203b79..e0b34f2f378d 100644 --- a/packages/p2p/src/components/rating-modal/rating-modal.jsx +++ b/packages/p2p/src/components/rating-modal/rating-modal.jsx @@ -25,6 +25,7 @@ const RatingModal = ({ is_open={is_rating_modal_open} title={localize('How would you rate this transaction?')} toggleModal={onClickSkip} + width={isMobile() && '90vw'} >
@@ -38,7 +39,7 @@ const RatingModal = ({ onClick={onClickStar} rating_value={rating_value} should_allow_half_icon={false} - star_size={isMobile() ? 17 : 20} + star_size={isMobile() ? 25 : 20} />
{rating_value > 0 && ( diff --git a/packages/p2p/src/stores/order-store.js b/packages/p2p/src/stores/order-store.js index 66a09790dd44..87ba18d5688f 100644 --- a/packages/p2p/src/stores/order-store.js +++ b/packages/p2p/src/stores/order-store.js @@ -23,7 +23,12 @@ export default class OrderStore { @observable cancels_remaining = null; @observable error_message = ''; @observable has_more_items_to_load = false; + @observable is_email_link_blocked_modal_open = false; + @observable is_email_link_verified_modal_open = false; + @observable is_email_verification_modal_open = false; + @observable is_invalid_verification_link_modal_open = false; @observable is_loading = false; + @observable is_loading_modal_open = false; @observable is_rating_modal_open = false; @observable is_recommended = undefined; @observable orders = []; @@ -31,6 +36,9 @@ export default class OrderStore { @observable order_payment_method_details = null; @observable order_rerender_timeout = null; @observable rating_value = 0; + @observable user_email_address = ''; + @observable verification_code = ''; + @observable verification_link_error_message = ''; interval; order_info_subscription = {}; @@ -45,7 +53,6 @@ export default class OrderStore { get order_information() { const { general_store } = this.root_store; const order = this.orders.find(o => o.id === this.order_id); - return order ? createExtendedOrderDetails(order, general_store.client.loginid, general_store.props.server_time) : null; @@ -57,15 +64,63 @@ export default class OrderStore { } @action.bound - confirmOrderRequest(id) { + confirmOrderRequest(id, is_buy_order_for_user) { const { order_details_store } = this.root_store; - requestWS({ p2p_order_confirm: 1, id, }).then(response => { - if (response && response.error) { - order_details_store.setErrorMessage(response.error.message); + if (response) { + if (response.error) { + if (response.error.code === 'OrderEmailVerificationRequired') { + clearTimeout(wait); + const wait = setTimeout(() => this.setIsEmailVerificationModalOpen(true), 250); + } else if ( + response?.error.code === 'InvalidVerificationToken' || + response?.error.code === 'ExcessiveVerificationRequests' + ) { + clearTimeout(wait); + if (this.is_email_verification_modal_open) { + this.setIsEmailVerificationModalOpen(false); + } + if (this.is_email_link_verified_modal_open) { + this.setIsEmailLinkVerifiedModalOpen(false); + } + this.setVerificationLinkErrorMessage(response.error.message); + const wait = setTimeout(() => this.setIsInvalidVerificationLinkModalOpen(true), 230); + } else if (response?.error.code === 'ExcessiveVerificationFailures') { + if (this.is_invalid_verification_link_modal_open) { + this.setIsInvalidVerificationLinkModalOpen(false); + } + clearTimeout(wait); + this.setVerificationLinkErrorMessage(response.error.message); + const wait = setTimeout(() => this.setIsEmailLinkBlockedModalOpen(true), 230); + } else { + order_details_store.setErrorMessage(response.error.message); + } + } else if (!is_buy_order_for_user) { + this.setIsRatingModalOpen(true); + } + + localStorage.removeItem('verification_code.p2p_order_confirm'); + } + }); + } + + @action.bound + confirmOrder(is_buy_order_for_user) { + requestWS({ + p2p_order_confirm: 1, + id: this.order_id, + verification_code: this.verification_code, + }).then(response => { + if (response && !response.error) { + if (!is_buy_order_for_user) { + clearTimeout(wait); + const wait = setTimeout(() => { + this.setIsRatingModalOpen(true); + }, 230); + } } }); } @@ -79,10 +134,18 @@ export default class OrderStore { this.setCancelsRemaining(response.p2p_advertiser_info.cancels_remaining); } }); - this.getWebsiteStatus(setShouldShowCancelModal); } + @action.bound + getSettings() { + requestWS({ get_settings: 1 }).then(response => { + if (response && !response.error) { + this.setUserEmailAddress(response.get_settings.email); + } + }); + } + @action.bound getWebsiteStatus(setShouldShowCancelModal) { requestWS({ website_status: 1 }).then(response => { @@ -94,7 +157,6 @@ export default class OrderStore { this.setCancellationCountPeriod(p2p_config.cancellation_count_period); this.setCancellationLimit(p2p_config.cancellation_limit); } - if (typeof setShouldShowCancelModal === 'function') { setShouldShowCancelModal(true); } @@ -111,18 +173,15 @@ export default class OrderStore { if (should_navigate && this.nav) { this.root_store.general_store.redirectTo(this.nav.location); } - this.setOrderId(null); } @action.bound loadMoreOrders({ startIndex }) { this.setApiErrorMessage(''); - return new Promise(resolve => { const { general_store } = this.root_store; const active = general_store.is_active_tab ? 1 : 0; - requestWS({ p2p_order_list: 1, active, @@ -191,6 +250,11 @@ export default class OrderStore { } } + @action.bound + onPageReturn() { + this.hideDetails(true); + } + @action.bound onUnmount() { clearTimeout(this.order_rerender_timeout); @@ -198,6 +262,19 @@ export default class OrderStore { this.hideDetails(false); } + @action.bound + setOrderDetails(response) { + if (response) { + if (!response?.error) { + const { p2p_order_info } = response; + + this.setQueryDetails(p2p_order_info); + } else { + this.unsubscribeFromCurrentOrder(); + } + } + } + @action.bound setOrderRating(id) { const rating = this.rating_value / 20; @@ -217,6 +294,53 @@ export default class OrderStore { }); } + @action.bound + setQueryDetails(input_order) { + const { general_store } = this.root_store; + const order_information = createExtendedOrderDetails( + input_order, + general_store.client.loginid, + general_store.props.server_time + ); + this.setOrderId(order_information.id); // Sets the id in URL + if (order_information.is_active_order) { + general_store.setOrderTableType(order_list.ACTIVE); + } else { + general_store.setOrderTableType(order_list.INACTIVE); + } + if (order_information?.payment_method_details) { + this.setOrderPaymentMethodDetails(Object.values(order_information?.payment_method_details)); + } + // When viewing specific order, update its read state in localStorage. + const { notifications } = this.root_store.general_store.getLocalStorageSettingsForLoginId(); + + if (notifications.length) { + const notification = notifications.find(n => n.order_id === order_information.id); + + if (notification) { + notification.is_seen = true; + this.root_store.general_store.updateP2pNotifications(notifications); + } + } + + // Force a refresh of this order when it's expired to correctly + // reflect the status of the order. This is to work around a BE issue + // where they only expire contracts once a minute rather than on expiry time. + const { remaining_seconds } = order_information; + + if (remaining_seconds > 0) { + clearTimeout(this.order_rerender_timeout); + + this.setOrderRendererTimeout( + setTimeout(() => { + if (typeof this.forceRerenderFn === 'function') { + this.forceRerenderFn(order_information.id); + } + }, (remaining_seconds + 1) * 1000) + ); + } + } + @action.bound subscribeToCurrentOrder() { this.order_info_subscription = subscribeWS( @@ -281,8 +405,38 @@ export default class OrderStore { } @action.bound - setForceRerenderOrders(forceRerenderFn) { - this.forceRerenderFn = forceRerenderFn; + verifyEmailVerificationCode(verification_action, verification_code) { + if (verification_action === 'p2p_order_confirm' && verification_code) { + requestWS({ + p2p_order_confirm: 1, + id: this.order_id, + verification_code, + dry_run: 1, + }).then(response => { + this.setIsLoadingModalOpen(false); + if (response) { + if (!response.error) { + clearTimeout(wait); + const wait = setTimeout(() => this.setIsEmailLinkVerifiedModalOpen(true), 650); + } else if ( + response.error.code === 'InvalidVerificationToken' || + response.error.code === 'ExcessiveVerificationRequests' + ) { + clearTimeout(wait); + this.setVerificationLinkErrorMessage(response.error.message); + const wait = setTimeout(() => this.setIsInvalidVerificationLinkModalOpen(true), 750); + } else if (response.error.code === 'ExcessiveVerificationFailures') { + if (this.is_invalid_verification_link_modal_open) { + this.setIsInvalidVerificationLinkModalOpen(false); + } + clearTimeout(wait); + this.setVerificationLinkErrorMessage(response.error.message); + const wait = setTimeout(() => this.setIsEmailLinkBlockedModalOpen(true), 600); + } + localStorage.removeItem('verification_code.p2p_order_confirm'); + } + }); + } } @action.bound @@ -320,16 +474,46 @@ export default class OrderStore { this.error_message = error_message; } + @action.bound + setForceRerenderOrders(forceRerenderFn) { + this.forceRerenderFn = forceRerenderFn; + } + @action.bound setHasMoreItemsToLoad(has_more_items_to_load) { this.has_more_items_to_load = has_more_items_to_load; } + @action.bound + setIsEmailLinkBlockedModalOpen(is_email_link_blocked_modal_open) { + this.is_email_link_blocked_modal_open = is_email_link_blocked_modal_open; + } + + @action.bound + setIsEmailLinkVerifiedModalOpen(is_email_link_verified_modal_open) { + this.is_email_link_verified_modal_open = is_email_link_verified_modal_open; + } + + @action.bound + setIsEmailVerificationModalOpen(is_email_verification_modal_open) { + this.is_email_verification_modal_open = is_email_verification_modal_open; + } + + @action.bound + setIsInvalidVerificationLinkModalOpen(is_invalid_verification_link_modal_open) { + this.is_invalid_verification_link_modal_open = is_invalid_verification_link_modal_open; + } + @action.bound setIsLoading(is_loading) { this.is_loading = is_loading; } + @action.bound + setIsLoadingModalOpen(is_loading_modal_open) { + this.is_loading_modal_open = is_loading_modal_open; + } + @action.bound setIsRatingModalOpen(is_rating_modal_open) { this.is_rating_modal_open = is_rating_modal_open; @@ -341,19 +525,9 @@ export default class OrderStore { } @action.bound - setOrderPaymentMethodDetails(order_payment_method_details) { - this.order_payment_method_details = order_payment_method_details; - } - - @action.bound - setOrderDetails(response) { - if (!response.error) { - const { p2p_order_info } = response; - - this.setQueryDetails(p2p_order_info); - } else { - this.unsubscribeFromCurrentOrder(); - } + setOrders(orders) { + this.previous_orders = cloneObject(this.orders); + this.orders = orders; } @action.bound @@ -368,9 +542,8 @@ export default class OrderStore { } @action.bound - setOrders(orders) { - this.previous_orders = cloneObject(this.orders); - this.orders = orders; + setOrderPaymentMethodDetails(order_payment_method_details) { + this.order_payment_method_details = order_payment_method_details; } @action.bound @@ -379,54 +552,24 @@ export default class OrderStore { } @action.bound - setQueryDetails(input_order) { - const { general_store } = this.root_store; - const order_information = createExtendedOrderDetails( - input_order, - general_store.client.loginid, - general_store.props.server_time - ); - this.setOrderId(order_information.id); // Sets the id in URL - if (order_information.is_active_order) { - general_store.setOrderTableType(order_list.ACTIVE); - } else { - general_store.setOrderTableType(order_list.INACTIVE); - } - if (order_information?.payment_method_details) { - this.setOrderPaymentMethodDetails(Object.values(order_information?.payment_method_details)); - } - // When viewing specific order, update its read state in localStorage. - const { notifications } = this.root_store.general_store.getLocalStorageSettingsForLoginId(); - - if (notifications.length) { - const notification = notifications.find(n => n.order_id === order_information.id); - - if (notification) { - notification.is_seen = true; - this.root_store.general_store.updateP2pNotifications(notifications); - } - } - - // Force a refresh of this order when it's expired to correctly - // reflect the status of the order. This is to work around a BE issue - // where they only expire contracts once a minute rather than on expiry time. - const { remaining_seconds } = order_information; + setRatingValue(rating_value) { + this.rating_value = rating_value; + } - if (remaining_seconds > 0) { - clearTimeout(this.order_rerender_timeout); + @action.bound + setUserEmailAddress(user_email_address) { + this.user_email_address = user_email_address; + } - this.setOrderRendererTimeout( - setTimeout(() => { - if (typeof this.forceRerenderFn === 'function') { - this.forceRerenderFn(order_information.id); - } - }, (remaining_seconds + 1) * 1000) - ); - } + // This is only for the order confirmation request, + // since on confirmation the code is removed from the query params + @action.bound + setVerificationCode(verification_code) { + this.verification_code = verification_code; } @action.bound - setRatingValue(rating_value) { - this.rating_value = rating_value; + setVerificationLinkErrorMessage(verification_link_error_message) { + this.verification_link_error_message = verification_link_error_message; } }