diff --git a/packages/core/build/config.js b/packages/core/build/config.js index dec4a3e76f9f..d4186e820d5b 100644 --- a/packages/core/build/config.js +++ b/packages/core/build/config.js @@ -39,6 +39,15 @@ const copyConfig = base => { from: path.resolve(__dirname, '../node_modules/@deriv/cashier/dist/cashier/css/'), to: 'cashier/css', }, + { + from: path.resolve(__dirname, '../node_modules/@deriv/p2p/dist/p2p/js/'), + to: 'p2p/js', + }, + { + from: path.resolve(__dirname, '../node_modules/@deriv/p2p/dist/p2p/css/'), + to: 'p2p/css', + noErrorOnMissing: true, + }, { from: path.resolve(__dirname, '../node_modules/@deriv/cashier/dist/cashier/public'), to: 'cashier/public', diff --git a/packages/p2p/index.js b/packages/p2p/index.js index b58e228f6469..19df5bf53d63 100644 --- a/packages/p2p/index.js +++ b/packages/p2p/index.js @@ -1 +1 @@ -module.exports = require('./lib/index'); +module.exports = require('./dist/p2p/js/index'); diff --git a/packages/p2p/package.json b/packages/p2p/package.json index 8ee8fcbbad50..106e9aaa259d 100644 --- a/packages/p2p/package.json +++ b/packages/p2p/package.json @@ -2,7 +2,7 @@ "name": "@deriv/p2p", "version": "0.7.3", "description": "p2p cashier", - "main": "lib/index.js", + "main": "dist/p2p/js/index.js", "private": false, "publishConfig": { "access": "public" diff --git a/packages/p2p/src/components/__tests__/app-content.spec.js b/packages/p2p/src/components/__tests__/app-content.spec.js index 38f9e53a0338..6b64a0dccd36 100644 --- a/packages/p2p/src/components/__tests__/app-content.spec.js +++ b/packages/p2p/src/components/__tests__/app-content.spec.js @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { useStores } from 'Stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import AppContent from '../app-content.jsx'; jest.mock('Stores', () => ({ @@ -8,6 +9,11 @@ jest.mock('Stores', () => ({ useStores: jest.fn(), })); +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn(), +})); + jest.mock('@deriv/components', () => ({ ...jest.requireActual('@deriv/components'), Tabs: jest.fn(({ children }) => ( @@ -39,6 +45,10 @@ describe('', () => { useStores.mockImplementation(() => ({ general_store: mocked_store_values, })); + useModalManagerContext.mockImplementation(() => ({ + showModal: () => {}, + hideModal: () => {}, + })); render(); expect(screen.getByText('Tabs')).toBeInTheDocument(); diff --git a/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx b/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx index dc23925d59ec..569d2d377e32 100644 --- a/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx +++ b/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx @@ -7,9 +7,10 @@ import { useStores } from 'Stores'; import { buy_sell } from 'Constants/buy-sell'; import { localize, Localize } from 'Components/i18next'; import { generateEffectiveRate } from 'Utils/format-value'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import './advertiser-page.scss'; -const AdvertiserPageRow = ({ row: advert, showAdPopup }) => { +const AdvertiserPageRow = ({ row: advert }) => { const { advertiser_page_store, buy_sell_store, floating_rate_store, general_store } = useStores(); const { currency } = general_store.client; const { @@ -22,6 +23,7 @@ const AdvertiserPageRow = ({ row: advert, showAdPopup }) => { rate_type, rate, } = advert; + const { showModal } = useModalManagerContext(); const is_buy_advert = advertiser_page_store.counterparty_type === buy_sell.BUY; const is_my_advert = advertiser_page_store.advertiser_details_id === general_store.advertiser_id; @@ -37,7 +39,9 @@ const AdvertiserPageRow = ({ row: advert, showAdPopup }) => { const showAdForm = () => { buy_sell_store.setSelectedAdState(advert); - showAdPopup(advert); + showModal({ + key: 'BuySellModal', + }); }; if (isMobile()) { @@ -138,7 +142,6 @@ AdvertiserPageRow.displayName = 'AdvertiserPageRow'; AdvertiserPageRow.propTypes = { advert: PropTypes.object, row: PropTypes.object, - showAdPopup: PropTypes.func, }; export default observer(AdvertiserPageRow); diff --git a/packages/p2p/src/components/advertiser-page/advertiser-page.jsx b/packages/p2p/src/components/advertiser-page/advertiser-page.jsx index aed006b4cfcb..adfb70058426 100644 --- a/packages/p2p/src/components/advertiser-page/advertiser-page.jsx +++ b/packages/p2p/src/components/advertiser-page/advertiser-page.jsx @@ -5,10 +5,7 @@ import { reaction } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useStores } from 'Stores'; import { Localize, localize } from 'Components/i18next'; -import { buy_sell } from 'Constants/buy-sell'; import { my_profile_tabs } from 'Constants/my-profile-tabs'; -import RateChangeModal from 'Components/buy-sell/rate-change-modal.jsx'; -import BuySellModal from 'Components/buy-sell/buy-sell-modal.jsx'; import PageReturn from 'Components/page-return/page-return.jsx'; import RecommendedBy from 'Components/recommended-by'; import UserAvatar from 'Components/user/user-avatar/user-avatar.jsx'; @@ -18,14 +15,14 @@ import StarRating from 'Components/star-rating'; import AdvertiserPageDropdownMenu from './advertiser-page-dropdown-menu.jsx'; import TradeBadge from '../trade-badge/trade-badge.jsx'; import BlockUserOverlay from './block-user/block-user-overlay'; -import BlockUserModal from 'Components/block-user/block-user-modal'; -import ErrorModal from 'Components/error-modal/error-modal'; import classNames from 'classnames'; import { OnlineStatusIcon, OnlineStatusLabel } from 'Components/online-status'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import './advertiser-page.scss'; const AdvertiserPage = () => { const { general_store, advertiser_page_store, buy_sell_store, my_profile_store } = useStores(); + const { hideModal, showModal, useRegisterModalProps } = useModalManagerContext(); const is_my_advert = advertiser_page_store.advertiser_details_id === general_store.advertiser_id; // Use general_store.advertiser_info since resubscribing to the same id from advertiser page returns error @@ -51,7 +48,6 @@ const AdvertiserPage = () => { // rating_average_decimal converts rating_average to 1 d.p number const rating_average_decimal = rating_average ? Number(rating_average).toFixed(1) : null; const joined_since = daysSince(created_time); - const [is_error_modal_open, setIsErrorModalOpen] = React.useState(false); React.useEffect(() => { advertiser_page_store.onMount(); @@ -61,7 +57,26 @@ const AdvertiserPage = () => { () => [advertiser_page_store.active_index, general_store.block_unblock_user_error], () => { advertiser_page_store.onTabChange(); - if (general_store.block_unblock_user_error) setIsErrorModalOpen(true); + if (general_store.block_unblock_user_error) { + showModal({ + key: 'ErrorModal', + props: { + error_message: general_store.block_unblock_user_error, + error_modal_title: 'Unable to block advertiser', + has_close_icon: false, + onClose: () => { + buy_sell_store.hideAdvertiserPage(); + if (general_store.active_index !== 0) + my_profile_store.setActiveTab(my_profile_tabs.MY_COUNTERPARTIES); + advertiser_page_store.onCancel(); + general_store.setBlockUnblockUserError(''); + hideModal(); + }, + width: isMobile() ? '90rem' : '40rem', + }, + }); + general_store.setBlockUnblockUserError(null); + } }, { fireImmediately: true } ); @@ -73,6 +88,16 @@ const AdvertiserPage = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useRegisterModalProps({ + key: 'BlockUserModal', + props: { + advertiser_name: name, + is_advertiser_blocked: !!advertiser_page_store.is_counterparty_advertiser_blocked && !is_my_advert, + onCancel: advertiser_page_store.onCancel, + onSubmit: advertiser_page_store.onSubmit, + }, + }); + if (advertiser_page_store.is_loading || general_store.is_block_unblock_user_loading) { return ; } @@ -88,36 +113,6 @@ const AdvertiserPage = () => { !!advertiser_page_store.is_counterparty_advertiser_blocked && !is_my_advert, })} > - - { - if (!is_open) buy_sell_store.hideAdvertiserPage(); - if (general_store.active_index !== 0) - my_profile_store.setActiveTab(my_profile_tabs.MY_COUNTERPARTIES); - advertiser_page_store.onCancel(); - general_store.setBlockUnblockUserError(''); - }} - width={isMobile() ? '90rem' : '40rem'} - /> - -
{
general_store.setIsBlockUserModalOpen(true)} + onClickUnblock={() => + showModal({ + key: 'BlockUserModal', + }) + } >
diff --git a/packages/p2p/src/components/app-content.jsx b/packages/p2p/src/components/app-content.jsx index 7e0c7b031559..118d0252ae10 100644 --- a/packages/p2p/src/components/app-content.jsx +++ b/packages/p2p/src/components/app-content.jsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { isMobile } from '@deriv/shared'; import { Loading, Tabs } from '@deriv/components'; +import { isAction, reaction } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useStores } from 'Stores'; import AdvertiserPage from 'Components/advertiser-page/advertiser-page.jsx'; @@ -13,9 +14,32 @@ import NicknameForm from './nickname-form'; import Orders from './orders/orders.jsx'; import TemporarilyBarredHint from './temporarily-barred-hint'; import Verification from './verification/verification.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; -const AppContent = () => { +const AppContent = ({ order_id }) => { const { buy_sell_store, general_store } = useStores(); + const { showModal, hideModal } = useModalManagerContext(); + + React.useEffect(() => { + return reaction( + () => general_store.props.setP2POrderProps, + () => { + if (isAction(general_store.props.setP2POrderProps)) { + general_store.props.setP2POrderProps({ + order_id, + redirectToOrderDetails: general_store.redirectToOrderDetails, + setIsRatingModalOpen: is_open => { + if (is_open) { + showModal({ key: 'RatingModal' }); + } else { + hideModal(); + } + }, + }); + } + } + ); + }, []); if (general_store.is_loading) { return ; diff --git a/packages/p2p/src/components/app.jsx b/packages/p2p/src/components/app.jsx index c718f88deef9..87348a2a3dd0 100644 --- a/packages/p2p/src/components/app.jsx +++ b/packages/p2p/src/components/app.jsx @@ -8,6 +8,7 @@ import { waitWS } from 'Utils/websocket'; import { useStores } from 'Stores'; import AppContent from './app-content.jsx'; import { setLanguage } from './i18next'; +import { ModalManager, ModalManagerContextProvider } from './modal-manager'; import './app.scss'; const App = props => { @@ -67,11 +68,7 @@ const App = props => { general_store.redirectTo('orders'); order_store.setOrderId(order_id); } - general_store.props.setP2POrderProps({ - order_id, - redirectToOrderDetails: general_store.redirectToOrderDetails, - setIsRatingModalOpen: order_store.setIsRatingModalOpen, - }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [order_id]); @@ -91,7 +88,7 @@ const App = props => { order_store.setVerificationCode(verification_code); } if (verification_action && verification_code) { - order_store.setIsLoadingModalOpen(true); + general_store.showModal({ key: 'LoadingModal', props: {} }); order_store.verifyEmailVerificationCode(verification_action, verification_code); } @@ -101,7 +98,10 @@ const App = props => { return (
- + + + +
); }; diff --git a/packages/p2p/src/components/buy-sell/buy-sell-form.jsx b/packages/p2p/src/components/buy-sell/buy-sell-form.jsx index 563d6d7c7ea1..7af4c4db7ee4 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-form.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-form.jsx @@ -14,12 +14,14 @@ import PaymentMethodCard from '../my-profile/payment-methods/payment-method-card import { floatingPointValidator } from 'Utils/validations'; import { countDecimalPlaces } from 'Utils/string'; import { generateEffectiveRate, setDecimalPlaces, roundOffDecimal, removeTrailingZeros } from 'Utils/format-value'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const BuySellForm = props => { const isMounted = useIsMounted(); const { advertiser_page_store, buy_sell_store, floating_rate_store, general_store, my_profile_store } = useStores(); const [selected_methods, setSelectedMethods] = React.useState([]); buy_sell_store.setFormProps(props); + const { showModal } = useModalManagerContext(); const { setPageFooterParent } = props; const { @@ -80,12 +82,25 @@ const BuySellForm = props => { } advertiser_page_store.setFormErrorMessage(''); - buy_sell_store.setShowRateChangePopup(rate_type === ad_type.FLOAT); + const disposeRateChangeModal = reaction( + () => floating_rate_store.is_market_rate_changed, + is_market_rate_changed => { + if (is_market_rate_changed && rate_type === ad_type.FLOAT) { + showModal({ + key: 'RateChangeModal', + props: { + currency: buy_sell_store.local_currency, + }, + }); + } + } + ); buy_sell_store.setInitialReceiveAmount(calculated_rate); return () => { buy_sell_store.payment_method_ids = []; disposeReceiveAmountReaction(); + disposeRateChangeModal(); }; }, [] // eslint-disable-line react-hooks/exhaustive-deps diff --git a/packages/p2p/src/components/buy-sell/buy-sell-header.jsx b/packages/p2p/src/components/buy-sell/buy-sell-header.jsx index 2d6a5827e1b3..e583655e3617 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-header.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-header.jsx @@ -102,7 +102,7 @@ const BuySellHeader = ({ table_type }) => { buy_sell_store.setIsFilterModalOpen(true)} + onClick={() => general_store.showModal({ key: 'FilterModal', props: {} })} size={40} />
diff --git a/packages/p2p/src/components/buy-sell/buy-sell-table.jsx b/packages/p2p/src/components/buy-sell/buy-sell-table.jsx index 2b8da108c4be..848bb54b36b6 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-table.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-table.jsx @@ -8,7 +8,6 @@ import { Localize } from 'Components/i18next'; import { TableError } from 'Components/table/table-error.jsx'; import { useStores } from 'Stores'; import BuySellRow from './buy-sell-row.jsx'; -import CancelAddPaymentMethodModal from '../my-profile/payment-methods/add-payment-method/cancel-add-payment-method-modal.jsx'; import NoAds from './no-ads/no-ads.jsx'; const BuySellRowRendererComponent = row_props => { @@ -31,7 +30,7 @@ const BuySellTable = ({ onScroll }) => { React.useEffect( () => { - my_profile_store.setIsCancelAddPaymentMethodModalOpen(false); + my_profile_store.getPaymentMethodsList(); reaction( () => buy_sell_store.is_buy, () => buy_sell_store.fetchAdvertiserAdverts(), @@ -53,7 +52,6 @@ const BuySellTable = ({ onScroll }) => { if (buy_sell_store.items.length) { return ( <> - { @@ -51,7 +47,6 @@ const BuySell = () => { return (
- { showAdvertiserPage={buy_sell_store.showAdvertiserPage} onScroll={onScroll} /> - - -
); }; diff --git a/packages/p2p/src/components/buy-sell/currency-dropdown.jsx b/packages/p2p/src/components/buy-sell/currency-dropdown.jsx index c0cc1d269ab7..a7fea5b9e3b8 100644 --- a/packages/p2p/src/components/buy-sell/currency-dropdown.jsx +++ b/packages/p2p/src/components/buy-sell/currency-dropdown.jsx @@ -5,14 +5,15 @@ import { Dropdown, useOnClickOutside } from '@deriv/components'; import { isMobile } from '@deriv/shared'; import { useStores } from 'Stores'; import { CurrencySelector } from 'Components/buy-sell/currency-selector'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import './currency-dropdown.scss'; const CurrencyDropdown = () => { const [is_list_visible, setIsListVisible] = React.useState(false); const currency_selector_ref = React.useRef(null); const { buy_sell_store } = useStores(); - const { local_currencies, onLocalCurrencySelect, selected_local_currency, setShouldShowCurrencySelectorModal } = - buy_sell_store; + const { local_currencies, onLocalCurrencySelect, selected_local_currency } = buy_sell_store; + const { showModal } = useModalManagerContext(); useOnClickOutside( currency_selector_ref, @@ -30,7 +31,7 @@ const CurrencyDropdown = () => { })} list={local_currencies} onClick={() => { - if (isMobile()) setShouldShowCurrencySelectorModal(true); + if (isMobile()) showModal({ key: 'CurrencySelectorModal' }); else setIsListVisible(!is_list_visible); }} value={selected_local_currency} diff --git a/packages/p2p/src/components/buy-sell/currency-selector/index.js b/packages/p2p/src/components/buy-sell/currency-selector/index.js index ce283be5bc6d..35e70dd8c8ca 100644 --- a/packages/p2p/src/components/buy-sell/currency-selector/index.js +++ b/packages/p2p/src/components/buy-sell/currency-selector/index.js @@ -1,5 +1,4 @@ import CurrencySelector from './currency-selector.jsx'; -import CurrencySelectorModal from './currency-selector-modal.jsx'; import './currency-selector.scss'; -export { CurrencySelector, CurrencySelectorModal }; +export { CurrencySelector }; diff --git a/packages/p2p/src/components/loading-modal/loading-modal.jsx b/packages/p2p/src/components/loading-modal/loading-modal.jsx deleted file mode 100644 index 7ff444d16f41..000000000000 --- a/packages/p2p/src/components/loading-modal/loading-modal.jsx +++ /dev/null @@ -1,17 +0,0 @@ -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/modal-manager/index.js b/packages/p2p/src/components/modal-manager/index.js new file mode 100644 index 000000000000..8f2a43096e07 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/index.js @@ -0,0 +1,4 @@ +import ModalManagerContextProvider from './modal-manager-context-provider'; +import ModalManager from './modal-manager'; + +export { ModalManagerContextProvider, ModalManager }; diff --git a/packages/p2p/src/components/modal-manager/modal-form/index.js b/packages/p2p/src/components/modal-manager/modal-form/index.js new file mode 100644 index 000000000000..a8c3146f3d9e --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modal-form/index.js @@ -0,0 +1,3 @@ +import ModalForm from './modal-form'; + +export default ModalForm; diff --git a/packages/p2p/src/components/modal-manager/modal-form/modal-form.jsx b/packages/p2p/src/components/modal-manager/modal-form/modal-form.jsx new file mode 100644 index 000000000000..b2458c1d3a47 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modal-form/modal-form.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { observer } from 'mobx-react-lite'; +import { useStores } from 'Stores'; + +/** + * Formik wrapper that automatically handles saving and restoring form state and values when a modal is unmounted + * Use this for modals with forms using Formik, that could be temporary hidden in place of another modal, + * such as an error modal or a message modal like CancelAddPaymentMethodModal + * + * Usage: Replace the Formik component with ModalForm, and pass Formik props to it + */ +const ModalForm = props => { + const { general_store } = useStores(); + + // using a callback ref instead of useRef() since useRef does not automatically update when content changes + const formik_ref = React.useCallback(node => { + if (node) general_store.setFormikRef(node); + }); + + React.useEffect(() => { + if (general_store.saved_form_state && general_store.formik_ref) { + general_store.formik_ref.setValues(general_store.saved_form_state.values); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {props.children} + + ); +}; + +export default observer(ModalForm); diff --git a/packages/p2p/src/components/modal-manager/modal-manager-context-provider.jsx b/packages/p2p/src/components/modal-manager/modal-manager-context-provider.jsx new file mode 100644 index 000000000000..fe4220add4d0 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modal-manager-context-provider.jsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { useStores } from 'Stores'; +import { ModalManagerContext } from './modal-manager-context'; +import { isDesktop } from '@deriv/shared'; + +const ModalManagerContextProvider = props => { + const [active_modal, setActiveModal] = React.useState({}); + const [previous_modal, setPreviousModal] = React.useState({}); + // for mobile, modals are stacked and not shown alternatingly one by one + const [stacked_modal, setStackedModal] = React.useState({}); + const [is_modal_open, setIsModalOpen] = React.useState(false); + const [modal_props, setModalProps] = React.useState(new Map()); + const { general_store } = useStores(); + + /** + * Sets the specified modals' props on mount or when the props passed to the hook has changed. + * + * Use this hook to declare the modals' props beforehand for cases when the props can't be passed/declared in stores. + * + * For instance, calling `showModal({key: ..., props: ... })` in a store action where the props can't be passed to the action, use this hook to pass the props beforehand + * and simply call `showModal({key: ...})` without the need to specify the props, since its already passed using this hook to the modal manager. + * + * @param {Object|Object[]} modals - list of object modals to set props, each modal object must contain a 'key' attribute and 'props' attribute + */ + const useRegisterModalProps = modals => { + const registered_modals = React.useRef([]); + + const registerModals = React.useCallback(() => { + if (Array.isArray(modals)) { + modals.forEach(modal => { + registered_modals.current.push(modal); + setModalProps(modal_props.set(modal.key, modal.props)); + }); + } else { + registered_modals.current.push(modals); + setModalProps(modal_props.set(modals.key, modals.props)); + } + }, [modals]); + + React.useEffect(() => { + registerModals(); + return () => { + registered_modals.current.forEach(registered_modal => { + modal_props.delete(registered_modal.key); + }); + registered_modals.current = []; + }; + }, [modals]); + }; + + /** + * Checks if the current visible modal matches the specified modal key passed to the argument. + * Can also be used to check for multiple modal keys. + * + * @param {...string} keys - the modal keys to check if the current visible modal matches it + */ + const isCurrentModal = (...keys) => keys.includes(active_modal.key); + + const showModal = modal => { + if (isDesktop()) { + setPreviousModal(active_modal); + setActiveModal(modal); + } else if (Object.keys(active_modal).length === 0) { + setActiveModal(modal); + } else { + setStackedModal(modal); + } + setIsModalOpen(true); + }; + + /** + * Hides the current shown modal. + * If a previous modal was present, by default the previous modal will be shown in-place of the current closed modal. + * This option can be overriden by setting `should_hide_all_modals` to `true` in the `options` argument to close all modals instead. + * + * @param {Object} options - list of supported settings to tweak how modals should be hidden: + * - **should_hide_all_modals**: `false` by default. If set to `true`, previous modal will not be shown and all modals are hidden. + * - **should_save_form_history**: `false` by default. If set to `true`, form values in modals that has a form with `ModalForm` component + * will be saved when the modal is hidden and restored when modal is shown again. + */ + const hideModal = (options = {}) => { + const { should_save_form_history = false, should_hide_all_modals = false } = options; + + if (should_save_form_history) { + general_store.saveFormState(); + } else { + general_store.setSavedFormState(null); + general_store.setFormikRef(null); + } + + if (isDesktop()) { + if (should_hide_all_modals) { + setPreviousModal({}); + setActiveModal({}); + setIsModalOpen(false); + } else if (previous_modal) { + setActiveModal(previous_modal); + setPreviousModal({}); + } else { + setActiveModal({}); + setIsModalOpen(false); + } + } else if (Object.keys(stacked_modal).length !== 0) { + if (should_hide_all_modals) { + setActiveModal({}); + setIsModalOpen(false); + } + setStackedModal({}); + } else { + setActiveModal({}); + setIsModalOpen(false); + } + }; + + general_store.hideModal = hideModal; + general_store.isCurrentModal = isCurrentModal; + general_store.modal = active_modal; + general_store.showModal = showModal; + + const state = { + hideModal, + is_modal_open, + modal: active_modal, + modal_props, + previous_modal, + stacked_modal, + showModal, + useRegisterModalProps, + }; + + general_store.showModal = showModal; + general_store.hideModal = hideModal; + general_store.modal = active_modal; + + return {props.children}; +}; + +export default ModalManagerContextProvider; diff --git a/packages/p2p/src/components/modal-manager/modal-manager-context.js b/packages/p2p/src/components/modal-manager/modal-manager-context.js new file mode 100644 index 000000000000..f934521a5ef1 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modal-manager-context.js @@ -0,0 +1,7 @@ +import React from 'react'; + +export const ModalManagerContext = React.createContext(); + +export const useModalManagerContext = () => { + return React.useContext(ModalManagerContext); +}; diff --git a/packages/p2p/src/components/modal-manager/modal-manager.jsx b/packages/p2p/src/components/modal-manager/modal-manager.jsx new file mode 100644 index 000000000000..4906d98f2b3d --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modal-manager.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { modals } from 'Constants/modals'; +import { useModalManagerContext } from './modal-manager-context'; + +const ModalManager = () => { + const { modal, modal_props, stacked_modal } = useModalManagerContext(); + + const { key } = modal; + const Modal = modals[key]; + const StackedModal = modals[stacked_modal?.key]; + + const getModalProps = current_modal => { + if (current_modal?.props && Object.keys(current_modal.props).length > 0) { + // if props was provided to the argument and it was also already initialised using useRegisterModalProps, + // merge the 2 props together and update latest prop values with the passed prop argument + if (modal_props.has(current_modal.key)) { + return { + ...modal_props.get(current_modal.key), + ...current_modal.props, + }; + } + return current_modal.props; + } + if (modal_props.has(current_modal.key)) { + return modal_props.get(current_modal.key); + } + return {}; + }; + + if (Modal) + return ( + + + {StackedModal && ( + + + + )} + + ); + + return null; +}; + +export default ModalManager; diff --git a/packages/p2p/src/components/my-ads/ad-exceeds-daily-limit-modal.jsx b/packages/p2p/src/components/modal-manager/modals/ad-exceeds-daily-limit-modal/ad-exceeds-daily-limit-modal.jsx similarity index 84% rename from packages/p2p/src/components/my-ads/ad-exceeds-daily-limit-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/ad-exceeds-daily-limit-modal/ad-exceeds-daily-limit-modal.jsx index 8ff67389a1c1..31b3e5763961 100644 --- a/packages/p2p/src/components/my-ads/ad-exceeds-daily-limit-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/ad-exceeds-daily-limit-modal/ad-exceeds-daily-limit-modal.jsx @@ -3,14 +3,16 @@ import { Button, Modal, Text } from '@deriv/components'; import { observer } from 'mobx-react-lite'; import { localize, Localize } from 'Components/i18next'; import { useStores } from 'Stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const AdExceedsDailyLimitModal = () => { const { my_ads_store } = useStores(); + const { hideModal, is_modal_open } = useModalManagerContext(); return ( { - + + + + ); +}; + +export default observer(CancelAddPaymentMethodModal); diff --git a/packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/cancel-edit-payment-method-modal.jsx b/packages/p2p/src/components/modal-manager/modals/cancel-edit-payment-method-modal.jsx similarity index 78% rename from packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/cancel-edit-payment-method-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/cancel-edit-payment-method-modal.jsx index 738141d1e1cc..b115ec8b4de0 100644 --- a/packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/cancel-edit-payment-method-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/cancel-edit-payment-method-modal.jsx @@ -3,14 +3,16 @@ import { Button, Modal, Text } from '@deriv/components'; import { observer } from 'mobx-react-lite'; import { useStores } from 'Stores'; import { Localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const CancelEditPaymentMethodModal = () => { const { my_profile_store } = useStores(); + const { hideModal, is_modal_open } = useModalManagerContext(); return ( @@ -27,21 +29,15 @@ const CancelEditPaymentMethodModal = () => { - diff --git a/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/create-ad-add-payment-method-modal.jsx b/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/create-ad-add-payment-method-modal.jsx new file mode 100644 index 000000000000..3a76fa98060b --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/create-ad-add-payment-method-modal.jsx @@ -0,0 +1,80 @@ +import classNames from 'classnames'; +import React from 'react'; +import { observer } from 'mobx-react-lite'; +import { Button, Icon, MobileFullPageModal, Modal } from '@deriv/components'; +import { isMobile } from '@deriv/shared'; +import { useStores } from 'Stores'; +import { localize, Localize } from 'Components/i18next'; +import AddPaymentMethod from 'Components/my-profile/payment-methods/add-payment-method/add-payment-method.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; + +const CreateAdAddPaymentMethodModal = () => { + const { general_store, my_profile_store } = useStores(); + const { hideModal, is_modal_open, showModal } = useModalManagerContext(); + + const onCancel = () => { + if (my_profile_store.selected_payment_method.length > 0 || general_store.is_form_modified) { + showModal({ + key: 'CancelAddPaymentMethodModal', + props: { + should_hide_all_modals_on_cancel: true, + }, + }); + } else { + hideModal({ + should_hide_all_modals: true, + }); + } + }; + + if (isMobile()) { + return ( + + + + ); + } + + return ( + + + {localize('Add payment method')} + + } + toggleModal={onCancel} + > + + + + {!my_profile_store.selected_payment_method && ( + + + + )} + + ); +}; + +export default observer(CreateAdAddPaymentMethodModal); diff --git a/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/create-ad-add-payment-method-modal.scss b/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/create-ad-add-payment-method-modal.scss new file mode 100644 index 000000000000..075e8349b7ae --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/create-ad-add-payment-method-modal.scss @@ -0,0 +1,61 @@ +.p2p-my-ads { + flex: 1; + display: flex; + flex-direction: column; + + @include mobile { + .page-return { + border-bottom: 2px solid var(--general-section-1); + margin: 1rem 0; + padding: 0 1.6rem 1.6rem; + } + + .dc-list { + margin-left: 1vw; + width: 88vw; + + &__item { + padding-top: 0.4rem; + } + } + .dc-mobile-full-page-modal { + opacity: 1 !important; + } + } + + &__modal { + &-body { + display: flex; + flex-direction: column; + padding: 1rem 1.6rem 0; + width: 100vw; + + &--scroll { + overflow: auto; + } + + &--horizontal { + padding-right: 0rem; + } + } + + &-error { + .dc-modal-header__title--p2p-my-ads__modal-error { + padding: 2.4rem 2.4rem 0 !important; + } + .dc-modal__container_p2p-my-ads__modal-error { + .dc-modal-footer { + padding-top: 0 !important; + } + + .dc-modal-header { + padding-bottom: 1rem; + } + } + } + + &-icon { + margin-right: 0.8rem; + } + } +} diff --git a/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/index.js b/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/index.js new file mode 100644 index 000000000000..8b7468e9f26b --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/create-ad-add-payment-method-modal/index.js @@ -0,0 +1,4 @@ +import CreateAdAddPaymentMethodModal from './create-ad-add-payment-method-modal.jsx'; +import './create-ad-add-payment-method-modal.scss'; + +export default CreateAdAddPaymentMethodModal; diff --git a/packages/p2p/src/components/my-ads/create-ad-error-modal.jsx b/packages/p2p/src/components/modal-manager/modals/create-ad-error-modal/create-ad-error-modal.jsx similarity index 70% rename from packages/p2p/src/components/my-ads/create-ad-error-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/create-ad-error-modal/create-ad-error-modal.jsx index bbb15df4c579..beeab340f29a 100644 --- a/packages/p2p/src/components/my-ads/create-ad-error-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/create-ad-error-modal/create-ad-error-modal.jsx @@ -4,15 +4,17 @@ import { observer } from 'mobx-react-lite'; import { localize, Localize } from 'Components/i18next'; import { api_error_codes } from 'Constants/api-error-codes'; import { useStores } from 'Stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const CreateAdErrorModal = () => { const { my_ads_store } = useStores(); + const { hideModal, is_modal_open } = useModalManagerContext(); if (my_ads_store.error_code === api_error_codes.DUPLICATE_ADVERT) { return ( { - @@ -35,8 +30,7 @@ ErrorModal.propTypes = { error_message: PropTypes.string, error_modal_title: PropTypes.string, has_close_icon: PropTypes.bool, - is_error_modal_open: PropTypes.bool, - setIsErrorModalOpen: PropTypes.func, + onClose: PropTypes.func, width: PropTypes.string, }; diff --git a/packages/p2p/src/components/error-modal/error-modal.scss b/packages/p2p/src/components/modal-manager/modals/error-modal/error-modal.scss similarity index 100% rename from packages/p2p/src/components/error-modal/error-modal.scss rename to packages/p2p/src/components/modal-manager/modals/error-modal/error-modal.scss diff --git a/packages/p2p/src/components/error-modal/index.js b/packages/p2p/src/components/modal-manager/modals/error-modal/index.js similarity index 71% rename from packages/p2p/src/components/error-modal/index.js rename to packages/p2p/src/components/modal-manager/modals/error-modal/index.js index 1e38a565cb4c..8534913d0fc8 100644 --- a/packages/p2p/src/components/error-modal/index.js +++ b/packages/p2p/src/components/modal-manager/modals/error-modal/index.js @@ -1,3 +1,4 @@ import ErrorModal from './error-modal.jsx'; +import './error-modal.scss'; export default ErrorModal; diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal-header.jsx b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-header.jsx similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal-header.jsx rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-header.jsx diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal-no-results.jsx b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-no-results.jsx similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal-no-results.jsx rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-no-results.jsx diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal-no-results.scss b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-no-results.scss similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal-no-results.scss rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-no-results.scss diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal-search.jsx b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-search.jsx similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal-search.jsx rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-search.jsx diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal-search.scss b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-search.scss similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal-search.scss rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal-search.scss diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal.jsx b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal.jsx similarity index 95% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal.jsx index 5c75071946ab..afed4bde740e 100644 --- a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal.jsx @@ -6,9 +6,11 @@ import { useStores } from 'Stores'; import FilterModalHeader from './filter-modal-header.jsx'; import FilterModalSearch from './filter-modal-search.jsx'; import FilterModalNoResults from './filter-modal-no-results.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const FilterModal = () => { const { buy_sell_store, my_profile_store } = useStores(); + const { hideModal, is_modal_open } = useModalManagerContext(); const [selected_methods, setSelectedMethods] = React.useState([]); const [selected_methods_text, setSelectedMethodsText] = React.useState([]); @@ -39,7 +41,7 @@ const FilterModal = () => { const onBackdropClick = () => { buy_sell_store.setShowFilterPaymentMethods(false); - buy_sell_store.setIsFilterModalOpen(false); + hideModal(); my_profile_store.setSearchTerm(''); if (selected_methods_text.length && !buy_sell_store.selected_payment_method_value.length) { onClickClear(); @@ -94,7 +96,7 @@ const FilterModal = () => { has_close_icon height={'56rem'} title={} - is_open={buy_sell_store.is_filter_modal_open} + is_open={is_modal_open} toggleModal={onBackdropClick} width='44rem' > @@ -176,7 +178,10 @@ const FilterModal = () => { diff --git a/packages/p2p/src/components/buy-sell/filter-modal/filter-modal.scss b/packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal.scss similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/filter-modal.scss rename to packages/p2p/src/components/modal-manager/modals/filter-modal/filter-modal.scss diff --git a/packages/p2p/src/components/buy-sell/filter-modal/index.js b/packages/p2p/src/components/modal-manager/modals/filter-modal/index.js similarity index 100% rename from packages/p2p/src/components/buy-sell/filter-modal/index.js rename to packages/p2p/src/components/modal-manager/modals/filter-modal/index.js diff --git a/packages/p2p/src/components/invalid-verification-link-modal/index.js b/packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/index.js similarity index 100% rename from packages/p2p/src/components/invalid-verification-link-modal/index.js rename to packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/index.js diff --git a/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx b/packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/invalid-verification-link-modal.jsx similarity index 62% rename from packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/invalid-verification-link-modal.jsx index 640d18aef1a2..169cbac32ec6 100644 --- a/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/invalid-verification-link-modal.jsx @@ -2,30 +2,27 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Button, Icon, Modal, Text } from '@deriv/components'; import { Localize } from 'Components/i18next'; +import { useStores } from 'Stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const InvalidVerificationLinkModal = ({ - invalid_verification_link_error_message, - is_invalid_verification_link_modal_open, - onClickGetNewLinkButton, - setIsInvalidVerificationLinkModalOpen, + error_message, + order_id, // TODO: Uncomment when time is available in BE response // verification_link_expiry_time, }) => { + const { order_store } = useStores(); + const { hideModal, is_modal_open } = useModalManagerContext(); + return ( - <>} - toggleModal={() => setIsInvalidVerificationLinkModalOpen(false)} - width='440px' - > + <>} toggleModal={hideModal} width='440px'> - {invalid_verification_link_error_message} + {error_message} @@ -33,8 +30,8 @@ const InvalidVerificationLinkModal = ({ large primary onClick={() => { - setIsInvalidVerificationLinkModalOpen(false); - onClickGetNewLinkButton(); + hideModal(); + order_store.confirmOrderRequest(order_id); }} > @@ -45,10 +42,8 @@ const InvalidVerificationLinkModal = ({ }; InvalidVerificationLinkModal.propTypes = { - invalid_verification_link_error_message: PropTypes.string, - is_invalid_verification_link_modal_open: PropTypes.bool, - onClickGetNewLinkButton: PropTypes.func, - setIsInvalidVerificationLinkModalOpen: PropTypes.func, + error_message: PropTypes.string, + order_id: PropTypes.string, // TODO: Uncomment when time is available in BE response // verification_link_expiry_time: PropTypes.number, }; diff --git a/packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.scss b/packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/invalid-verification-link-modal.scss similarity index 100% rename from packages/p2p/src/components/invalid-verification-link-modal/invalid-verification-link-modal.scss rename to packages/p2p/src/components/modal-manager/modals/invalid-verification-link-modal/invalid-verification-link-modal.scss diff --git a/packages/p2p/src/components/loading-modal/index.js b/packages/p2p/src/components/modal-manager/modals/loading-modal/index.js similarity index 100% rename from packages/p2p/src/components/loading-modal/index.js rename to packages/p2p/src/components/modal-manager/modals/loading-modal/index.js diff --git a/packages/p2p/src/components/modal-manager/modals/loading-modal/loading-modal.jsx b/packages/p2p/src/components/modal-manager/modals/loading-modal/loading-modal.jsx new file mode 100644 index 000000000000..c4229ed3ffb5 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/loading-modal/loading-modal.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Loading, Modal } from '@deriv/components'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; + +const LoadingModal = () => { + const { is_modal_open } = useModalManagerContext(); + + return ( + + + + ); +}; + +export default LoadingModal; diff --git a/packages/p2p/src/components/modal-manager/modals/market-rate-change-error-modal/index.js b/packages/p2p/src/components/modal-manager/modals/market-rate-change-error-modal/index.js new file mode 100644 index 000000000000..31441ccbe4d0 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/market-rate-change-error-modal/index.js @@ -0,0 +1,3 @@ +import MarketRateChangeErrorModal from './market-rate-change-error-modal.jsx'; + +export default MarketRateChangeErrorModal; diff --git a/packages/p2p/src/components/modal-manager/modals/market-rate-change-error-modal/market-rate-change-error-modal.jsx b/packages/p2p/src/components/modal-manager/modals/market-rate-change-error-modal/market-rate-change-error-modal.jsx new file mode 100644 index 000000000000..960f932f4d69 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/market-rate-change-error-modal/market-rate-change-error-modal.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Button, Modal, Text } from '@deriv/components'; +import { isMobile } from '@deriv/shared'; +import { localize, Localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; + +const MarketRateChangeErrorModal = () => { + const { is_modal_open, hideModal } = useModalManagerContext(); + + return ( + + + + + + + + + + + + + ); +}; + +export default observer(MyAdsDeleteErrorModal); diff --git a/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/index.js b/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/index.js new file mode 100644 index 000000000000..a7da0e82cd49 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/index.js @@ -0,0 +1,4 @@ +import MyAdsDeleteModal from './my-ads-delete-modal.jsx'; +import './my-ads-delete-modal.scss'; + +export default MyAdsDeleteModal; diff --git a/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/my-ads-delete-modal.jsx b/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/my-ads-delete-modal.jsx new file mode 100644 index 000000000000..ef5a69f03d8b --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/my-ads-delete-modal.jsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Button, Modal, Text } from '@deriv/components'; +import { isDesktop } from '@deriv/shared'; +import { observer } from 'mobx-react-lite'; +import { Localize } from 'Components/i18next'; +import { requestWS } from 'Utils/websocket'; +import { useStores } from 'Stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; + +const MyAdsDeleteModal = () => { + const { my_ads_store } = useStores(); + const { hideModal, is_modal_open, showModal } = useModalManagerContext(); + + const onClickCancel = () => { + my_ads_store.setDeleteErrorMessage(''); + my_ads_store.setSelectedAdId(''); + hideModal(); + }; + + const onClickConfirm = () => { + hideModal(); + requestWS({ p2p_advert_update: 1, id: my_ads_store.selected_ad_id, delete: 1 }).then(response => { + if (response.error) { + my_ads_store.setDeleteErrorMessage(response.error.message); + showModal({ key: 'MyAdsDeleteErrorModal', props: {} }); + } else { + // remove the deleted ad from the list of items + const updated_items = my_ads_store.adverts.filter(ad => ad.id !== response.p2p_advert_update.id); + my_ads_store.setAdverts(updated_items); + } + }); + }; + + return ( + + ( + + + + )} + width='440px' + > + + + + + + + + + + + + ); +}; + +export default observer(MyAdsDeleteModal); diff --git a/packages/p2p/src/components/my-ads/my-ads-delete-modal.scss b/packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/my-ads-delete-modal.scss similarity index 100% rename from packages/p2p/src/components/my-ads/my-ads-delete-modal.scss rename to packages/p2p/src/components/modal-manager/modals/my-ads-delete-modal/my-ads-delete-modal.scss diff --git a/packages/p2p/src/components/modal-manager/modals/my-ads-floating-rate-switch-modal/index.js b/packages/p2p/src/components/modal-manager/modals/my-ads-floating-rate-switch-modal/index.js new file mode 100644 index 000000000000..ba2e72527a48 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/my-ads-floating-rate-switch-modal/index.js @@ -0,0 +1,3 @@ +import MyAdsFloatingRateSwitchModal from './my-ads-floating-rate-switch-modal.jsx'; + +export default MyAdsFloatingRateSwitchModal; diff --git a/packages/p2p/src/components/my-ads/my-ads-floating-rate-switch-modal.jsx b/packages/p2p/src/components/modal-manager/modals/my-ads-floating-rate-switch-modal/my-ads-floating-rate-switch-modal.jsx similarity index 93% rename from packages/p2p/src/components/my-ads/my-ads-floating-rate-switch-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/my-ads-floating-rate-switch-modal/my-ads-floating-rate-switch-modal.jsx index ad02e588963b..de80aa753b62 100644 --- a/packages/p2p/src/components/my-ads/my-ads-floating-rate-switch-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/my-ads-floating-rate-switch-modal/my-ads-floating-rate-switch-modal.jsx @@ -4,14 +4,16 @@ import { Button, Modal } from '@deriv/components'; import { observer } from 'mobx-react-lite'; import { ad_type } from 'Constants/floating-rate'; import { useStores } from 'Stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const MyAdsFloatingRateSwitchModal = () => { const { floating_rate_store, my_ads_store } = useStores(); + const { is_modal_open } = useModalManagerContext(); return ( my_ads_store.toggleMyAdsRateSwitchModal(my_ads_store.selected_ad_type)} small className='switch-ads' diff --git a/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/__tests__/order-details-cancel-modal.spec.js b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/__tests__/order-details-cancel-modal.spec.js new file mode 100644 index 000000000000..e00e79760ce8 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/__tests__/order-details-cancel-modal.spec.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { useStores } from 'Stores'; +import { requestWS } from 'Utils/websocket'; +import OrderDetailsCancelModal from '../order-details-cancel-modal.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context.js'; + +const el_modal = document.createElement('div'); + +jest.mock('Utils/websocket', () => ({ + ...jest.requireActual('Utils/websocket'), + requestWS: jest.fn().mockRejectedValue(), +})); + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + useIsMounted: jest.fn().mockReturnValue(() => true), +})); + +jest.mock('Stores', () => ({ + ...jest.requireActual('Stores'), + useStores: jest.fn().mockReturnValue({ + general_store: { + advertiser_info: { + cancels_remaining: 10, + }, + }, + order_store: { + cancellation_block_duration: '17', + cancellation_limit: '8', + cancellation_count_period: '19', + order_information: { + id: '10', + }, + setErrorMessage: jest.fn(), + }, + }), +})); + +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn().mockReturnValue({ + hideModal: jest.fn(), + is_modal_open: true, + }), +})); + +describe('', () => { + beforeAll(() => { + el_modal.setAttribute('id', 'modal_root'); + document.body.appendChild(el_modal); + }); + + afterAll(() => { + document.body.removeChild(el_modal); + }); + + it('should render cancel modal in desktop ', () => { + render(); + + expect(screen.getByText('Do you want to cancel this order?')).toBeInTheDocument(); + }); + + it('should warn the user if the number of remaining cancels is equal to 1 ', () => { + const { general_store } = useStores(); + general_store.advertiser_info.cancels_remaining = 1; + + render(); + + expect( + screen.getByText("If you cancel this order, you'll be blocked from using Deriv P2P for 17 hours.") + ).toBeInTheDocument(); + }); + + it('should not cancel the order and hide the modal if Do not Cancel button is clicked', () => { + const { hideModal } = useModalManagerContext(); + + render(); + fireEvent.click(screen.getByRole('button', { name: 'Do not cancel' })); + + expect(hideModal).toHaveBeenCalled(); + }); + + it('should cancel the order when Cancel this order button is clicked', () => { + requestWS.mockResolvedValue({ message: 'Success' }); + render(); + fireEvent.click(screen.getByRole('button', { name: 'Cancel this order' })); + + expect(requestWS).toHaveBeenCalled(); + }); + + it('should show error message when error response is received', async () => { + const error_msg = 'Error'; + const { order_store } = useStores(); + + requestWS.mockResolvedValue({ error: { message: error_msg } }); + + render(); + fireEvent.click(screen.getByRole('button', { name: 'Cancel this order' })); + + await waitFor(() => { + expect(order_store.setErrorMessage).toHaveBeenCalledWith(error_msg); + }); + }); +}); diff --git a/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/index.js b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/index.js new file mode 100644 index 000000000000..2883e34d7c5d --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/index.js @@ -0,0 +1,4 @@ +import OrderDetailsCancelModal from './order-details-cancel-modal.jsx'; +import './order-details-cancel-modal.scss'; + +export default OrderDetailsCancelModal; diff --git a/packages/p2p/src/components/order-details/order-details-cancel-modal.jsx b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/order-details-cancel-modal.jsx similarity index 84% rename from packages/p2p/src/components/order-details/order-details-cancel-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/order-details-cancel-modal.jsx index d1a9bd58269a..e5715e837613 100644 --- a/packages/p2p/src/components/order-details/order-details-cancel-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/order-details-cancel-modal.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { Button, Modal, Text } from '@deriv/components'; import { useIsMounted } from '@deriv/shared'; @@ -6,25 +5,26 @@ import { Localize } from 'Components/i18next'; import { requestWS } from 'Utils/websocket'; import { useStores } from 'Stores'; import FormError from 'Components/form/error.jsx'; -import 'Components/order-details/order-details-cancel-modal.scss'; +import { useModalManagerContext } from '../../modal-manager-context'; -const OrderDetailsCancelModal = ({ hideCancelOrderModal, order_id, should_show_cancel_modal }) => { +const OrderDetailsCancelModal = () => { const { general_store, order_store } = useStores(); const { cancels_remaining } = general_store.advertiser_info; + const { hideModal, is_modal_open } = useModalManagerContext(); const isMounted = useIsMounted(); const cancelOrderRequest = () => { requestWS({ p2p_order_cancel: 1, - id: order_id, + id: order_store.order_information.id, }).then(response => { if (isMounted()) { if (response.error) { order_store.setErrorMessage(response.error.message); } - hideCancelOrderModal(); + hideModal(); } }); }; @@ -33,8 +33,8 @@ const OrderDetailsCancelModal = ({ hideCancelOrderModal, order_id, should_show_c ( @@ -77,7 +77,7 @@ const OrderDetailsCancelModal = ({ hideCancelOrderModal, order_id, should_show_c - @@ -86,10 +86,4 @@ const OrderDetailsCancelModal = ({ hideCancelOrderModal, order_id, should_show_c ); }; -OrderDetailsCancelModal.propTypes = { - hideCancelOrderModal: PropTypes.func, - order_id: PropTypes.string, - should_show_cancel_modal: PropTypes.bool, -}; - export default OrderDetailsCancelModal; diff --git a/packages/p2p/src/components/order-details/order-details-cancel-modal.scss b/packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/order-details-cancel-modal.scss similarity index 100% rename from packages/p2p/src/components/order-details/order-details-cancel-modal.scss rename to packages/p2p/src/components/modal-manager/modals/order-details-cancel-modal/order-details-cancel-modal.scss diff --git a/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/__tests__/order-details-confirm-modal.spec.js b/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/__tests__/order-details-confirm-modal.spec.js new file mode 100644 index 000000000000..060e8a2f7ca3 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/__tests__/order-details-confirm-modal.spec.js @@ -0,0 +1,105 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { useStores } from 'Stores'; +import OrderDetailsConfirmModal from '../order-details-confirm-modal.jsx'; + +const el_modal = document.createElement('div'); + +jest.mock('Utils/websocket', () => ({ + ...jest.requireActual('Utils/websocket'), + requestWS: jest.fn().mockResolvedValue({ error: { message: 'P2P Error' } }), +})); + +jest.mock('Stores', () => ({ + ...jest.requireActual('Stores'), + useStores: jest.fn().mockReturnValue({ + order_details_store: { + error_message: '', + }, + order_store: { + confirmOrderRequest: jest.fn(), + order_information: { + account_currency: 'USD', + amount: 10, + amount_display: '20', + id: 1, + is_buy_order_for_user: false, + local_currency: 'AED', + other_user_details: { name: 'P2P' }, + rate: 2, + }, + }, + }), +})); + +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn(() => ({ + hideModal: jest.fn(), + is_modal_open: true, + })), +})); + +describe('', () => { + beforeAll(() => { + el_modal.setAttribute('id', 'modal_root'); + document.body.appendChild(el_modal); + }); + afterAll(() => { + document.body.removeChild(el_modal); + }); + + it('should render the modal', () => { + render(); + + expect(screen.getByText('Have you received payment?')).toBeInTheDocument(); + expect( + screen.getByText( + 'Please confirm only after checking your bank or e-wallet account to make sure you have received payment.' + ) + ).toBeInTheDocument(); + expect(screen.getByText("I've received 20.00 AED")).toBeInTheDocument(); + expect(screen.getByText('Cancel')).toBeInTheDocument(); + }); + + it('should render proper texts for buy order', () => { + const { order_store } = useStores(); + const { order_information } = order_store; + order_information.is_buy_order_for_user = true; + + render(); + + expect(screen.getByText('Payment confirmation')).toBeInTheDocument(); + expect(screen.getByText('Have you paid 20.00 AED to P2P?')).toBeInTheDocument(); + expect(screen.getByText("Yes, I've paid")).toBeInTheDocument(); + expect(screen.getByText("I haven't paid yet")).toBeInTheDocument(); + }); + + it('should enable the release button if user confirms receiving of amount', () => { + const { order_store } = useStores(); + const { order_information } = order_store; + order_information.is_buy_order_for_user = false; + + render(); + + fireEvent.click(screen.getByRole('checkbox')); + expect(screen.getByRole('button', { name: 'Release 20 USD' })).toBeEnabled(); + }); + + it('should send a request when release button is clicked', async () => { + const { order_store } = useStores(); + const { confirmOrderRequest, order_information } = order_store; + + render(); + + fireEvent.click(screen.getByRole('checkbox')); + fireEvent.click(screen.getByRole('button', { name: 'Release 20 USD' })); + + await waitFor(() => { + expect(confirmOrderRequest).toHaveBeenCalledWith( + order_information.id, + order_information.is_buy_order_for_user + ); + }); + }); +}); diff --git a/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/index.js b/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/index.js new file mode 100644 index 000000000000..75cf94729d7c --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/index.js @@ -0,0 +1,3 @@ +import OrderDetailsConfirmModal from './order-details-confirm-modal.jsx'; + +export default OrderDetailsConfirmModal; diff --git a/packages/p2p/src/components/order-details/order-details-confirm-modal.jsx b/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/order-details-confirm-modal.jsx similarity index 86% rename from packages/p2p/src/components/order-details/order-details-confirm-modal.jsx rename to packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/order-details-confirm-modal.jsx index 593e191d4244..b063e21eec38 100644 --- a/packages/p2p/src/components/order-details/order-details-confirm-modal.jsx +++ b/packages/p2p/src/components/modal-manager/modals/order-details-confirm-modal/order-details-confirm-modal.jsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import { Button, Checkbox, Modal, Text } from '@deriv/components'; import { useStores } from 'Stores'; @@ -6,28 +5,30 @@ import { Localize } from 'Components/i18next'; import FormError from 'Components/form/error.jsx'; import 'Components/order-details/order-details-confirm-modal.scss'; import { setDecimalPlaces, roundOffDecimal } from 'Utils/format-value'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; -const OrderDetailsConfirmModal = ({ - order_information, - is_buy_order_for_user, - hideConfirmOrderModal, - should_show_confirm_modal, -}) => { - const { account_currency, amount, amount_display, id, local_currency, other_user_details, rate } = - order_information; - +const OrderDetailsConfirmModal = () => { + const { hideModal, is_modal_open } = useModalManagerContext(); const { order_details_store, order_store } = useStores(); - + const { + account_currency, + amount, + amount_display, + id, + is_buy_order_for_user, + local_currency, + other_user_details, + rate, + } = order_store.order_information; const [is_checkbox_checked, setIsCheckboxChecked] = React.useState(false); - const rounded_rate = roundOffDecimal(rate, setDecimalPlaces(rate, 6)); return ( ( @@ -55,7 +56,6 @@ const OrderDetailsConfirmModal = ({ )} - setIsCheckboxChecked(!is_checkbox_checked)} @@ -78,7 +78,7 @@ const OrderDetailsConfirmModal = ({ {order_details_store.error_message && } - @@ -68,15 +67,10 @@ const RatingModal = ({ RatingModal.propTypes = { is_buy_order_for_user: PropTypes.bool, - is_rating_modal_open: PropTypes.bool, is_user_recommended_previously: PropTypes.number, - onClickClearRecommendation: PropTypes.func, onClickDone: PropTypes.func, - onClickNotRecommended: PropTypes.func, - onClickRecommended: PropTypes.func, onClickSkip: PropTypes.func, onClickStar: PropTypes.func, - rating_value: PropTypes.number, }; -export default React.memo(RatingModal); +export default observer(RatingModal); diff --git a/packages/p2p/src/components/rating-modal/rating-modal.scss b/packages/p2p/src/components/modal-manager/modals/rating-modal/rating-modal.scss similarity index 100% rename from packages/p2p/src/components/rating-modal/rating-modal.scss rename to packages/p2p/src/components/modal-manager/modals/rating-modal/rating-modal.scss diff --git a/packages/p2p/src/components/modal-manager/modals/recommended-modal/index.js b/packages/p2p/src/components/modal-manager/modals/recommended-modal/index.js new file mode 100644 index 000000000000..b7b9a7c65b0f --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/recommended-modal/index.js @@ -0,0 +1,4 @@ +import './recommended-modal.scss'; +import RecommendedModal from './recommended-modal'; + +export default RecommendedModal; diff --git a/packages/p2p/src/components/modal-manager/modals/recommended-modal/recommended-modal.jsx b/packages/p2p/src/components/modal-manager/modals/recommended-modal/recommended-modal.jsx new file mode 100644 index 000000000000..047afce684c5 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/recommended-modal/recommended-modal.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, Text } from '@deriv/components'; +import { localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; + +const RecommendedModal = ({ message }) => { + const { hideModal, is_modal_open } = useModalManagerContext(); + + return ( + + + + {message} + + + + - - )} - - ); -}; - -export default observer(CreateAdAddPaymentMethodModal); diff --git a/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx b/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx index de7d8570db4a..d805e259111c 100644 --- a/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx +++ b/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx @@ -5,11 +5,13 @@ import PaymentMethodCard from '../my-profile/payment-methods/payment-method-card import { localize } from 'Components/i18next'; import BuyAdPaymentMethodsList from './buy-ad-payment-methods-list.jsx'; import SellAdPaymentMethodsList from './sell-ad-payment-methods-list.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const CreateAdFormPaymentMethods = ({ is_sell_advert, onSelectPaymentMethods }) => { const { my_ads_store, my_profile_store } = useStores(); const [selected_buy_methods, setSelectedBuyMethods] = React.useState([]); const [selected_sell_methods, setSelectedSellMethods] = React.useState([]); + const { showModal } = useModalManagerContext(); const onClickPaymentMethodCard = payment_method => { if (!my_ads_store.payment_method_ids.includes(payment_method.ID)) { @@ -49,7 +51,11 @@ const CreateAdFormPaymentMethods = ({ is_sell_advert, onSelectPaymentMethods }) return ( my_ads_store.setShouldShowAddPaymentMethodModal(true)} + onClickAdd={() => + showModal({ + key: 'CreateAdAddPaymentMethodModal', + }) + } onClickPaymentMethodCard={onClickPaymentMethodCard} /> ); @@ -60,13 +66,21 @@ const CreateAdFormPaymentMethods = ({ is_sell_advert, onSelectPaymentMethods }) is_add label={localize('Payment method')} medium - onClickAdd={() => my_ads_store.setShouldShowAddPaymentMethodModal(true)} + onClickAdd={() => + showModal({ + key: 'CreateAdAddPaymentMethodModal', + }) + } /> ); } return ( - + ); }; diff --git a/packages/p2p/src/components/my-ads/create-ad-form.jsx b/packages/p2p/src/components/my-ads/create-ad-form.jsx index de83c2069e6a..6cb2621f14ca 100644 --- a/packages/p2p/src/components/my-ads/create-ad-form.jsx +++ b/packages/p2p/src/components/my-ads/create-ad-form.jsx @@ -19,9 +19,8 @@ import { buy_sell } from 'Constants/buy-sell'; import { ad_type } from 'Constants/floating-rate'; import { useStores } from 'Stores'; import CreateAdSummary from './create-ad-summary.jsx'; -import CreateAdErrorModal from './create-ad-error-modal.jsx'; import CreateAdFormPaymentMethods from './create-ad-form-payment-methods.jsx'; -import CreateAdAddPaymentMethodModal from './create-ad-add-payment-method-modal.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const CreateAdFormWrapper = ({ children }) => { if (isMobile()) { @@ -36,6 +35,7 @@ const CreateAdForm = () => { const { currency, local_currency_config } = general_store.client; const should_not_show_auto_archive_message_again = React.useRef(false); const [selected_methods, setSelectedMethods] = React.useState([]); + const { useRegisterModalProps } = useModalManagerContext(); // eslint-disable-next-line no-shadow const handleSelectPaymentMethods = selected_methods => { @@ -51,19 +51,29 @@ const CreateAdForm = () => { JSON.stringify(should_not_show_auto_archive_message_again.current) ); my_ads_store.setIsAdCreatedModalVisible(false); - if (my_ads_store.advert_details?.visibility_status?.includes('advertiser_daily_limit')) { - my_ads_store.setIsAdExceedsDailyLimitModalOpen(true); - } + if (my_ads_store.advert_details?.visibility_status?.includes('advertiser_daily_limit')) + general_store.showModal({ key: 'AdExceedsDailyLimitModal', props: {} }); + my_ads_store.setShowAdForm(false); }; + // when adding payment methods in creating an ad, once user declines to save their payment method, flow is to close all add payment method modals + useRegisterModalProps({ + key: 'CancelAddPaymentMethodModal', + props: { + should_hide_all_modals_on_cancel: true, + }, + }); + React.useEffect(() => { my_ads_store.setCurrentMethod({ key: null, is_deleted: false }); my_profile_store.getPaymentMethodsList(); my_profile_store.getAdvertiserPaymentMethods(); const disposeApiErrorReaction = reaction( () => my_ads_store.api_error_message, - () => my_ads_store.setIsApiErrorModalVisible(!!my_ads_store.api_error_message) + () => { + if (my_ads_store.api_error_message) general_store.showModal({ key: 'CreateAdErrorModal', props: {} }); + } ); // P2P configuration is not subscribable. Hence need to fetch it on demand general_store.setP2PConfig(); @@ -72,6 +82,7 @@ const CreateAdForm = () => { disposeApiErrorReaction(); my_ads_store.setApiErrorMessage(''); floating_rate_store.setApiErrorMessage(''); + my_ads_store.setShowAdForm(false); buy_sell_store.setCreateSellAdFromNoAds(false); }; @@ -422,8 +433,6 @@ const CreateAdForm = () => { ); }} - - { - return ( - - - - {localize('If you choose to cancel, the edited details will be lost.')} - - - - - - - - - ( - - - - )} - width='440px' - > - {my_ads_store.delete_error_message} - - - - - - - - ); -}; - -MyAdsDeleteModal.propTypes = { - is_delete_modal_open: PropTypes.bool, - setIsDeleteModalOpen: PropTypes.func, -}; - -export default observer(MyAdsDeleteModal); diff --git a/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx b/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx index 4587010dd9b5..ba2c545bc9c3 100644 --- a/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx +++ b/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx @@ -12,7 +12,7 @@ import { useStores } from 'Stores'; import { generateEffectiveRate } from 'Utils/format-value'; import AdType from './ad-type.jsx'; -const MyAdsRowRenderer = observer(({ row: advert, setAdvert }) => { +const MyAdsRowRenderer = observer(({ row: advert }) => { const { floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); const { @@ -62,13 +62,17 @@ const MyAdsRowRenderer = observer(({ row: advert, setAdvert }) => { }; const onClickAdd = () => { if (general_store.is_listed && !general_store.is_barred) { - setAdvert(advert); my_ads_store.showQuickAddModal(advert); } }; const onClickDelete = () => !general_store.is_barred && my_ads_store.onClickDelete(id); const onClickEdit = () => !general_store.is_barred && my_ads_store.onClickEdit(id, rate_type); - const onClickSwitchAd = () => !general_store.is_barred && my_ads_store.setIsSwitchModalOpen(true, id); + const onClickSwitchAd = () => { + if (!general_store.is_barred) { + general_store.showModal({ key: 'MyAdsFloatingRateSwitchModal', props: {} }); + my_ads_store.onToggleSwitchModal(id); + } + }; const onMouseEnter = () => setIsPopoverActionsVisible(true); const onMouseLeave = () => setIsPopoverActionsVisible(false); diff --git a/packages/p2p/src/components/my-ads/my-ads-table.jsx b/packages/p2p/src/components/my-ads/my-ads-table.jsx index 5502e2012c05..82e0a59a26b9 100644 --- a/packages/p2p/src/components/my-ads/my-ads-table.jsx +++ b/packages/p2p/src/components/my-ads/my-ads-table.jsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import React from 'react'; -import { Button, HintBox, InfiniteDataList, Loading, Modal, Table, Text } from '@deriv/components'; +import { Button, HintBox, InfiniteDataList, Loading, Table, Text } from '@deriv/components'; import { isDesktop, isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; import { localize, Localize } from 'Components/i18next'; @@ -9,12 +9,7 @@ import ToggleAds from 'Components/my-ads/toggle-ads.jsx'; import { TableError } from 'Components/table/table-error.jsx'; import { ad_type } from 'Constants/floating-rate'; import { useStores } from 'Stores'; -import { generateErrorDialogTitle } from 'Utils/adverts'; -import MyAdsDeleteModal from './my-ads-delete-modal.jsx'; -import MyAdsFloatingRateSwitchModal from './my-ads-floating-rate-switch-modal.jsx'; import MyAdsRowRenderer from './my-ads-row-renderer.jsx'; -import QuickAddModal from './quick-add-modal.jsx'; -import AdExceedsDailyLimitModal from './ad-exceeds-daily-limit-modal.jsx'; const getHeaders = offered_currency => [ { text: localize('Ad ID') }, @@ -52,7 +47,6 @@ const AdSwitchHintBox = () => { const MyAdsTable = () => { const { floating_rate_store, general_store, my_ads_store } = useStores(); - const [selected_advert, setSelectedAdvert] = React.useState(undefined); React.useEffect(() => { my_ads_store.setAdverts([]); @@ -77,7 +71,6 @@ const MyAdsTable = () => { if (my_ads_store.adverts.length) { return ( - {selected_advert && } {floating_rate_store.change_ad_alert && (
{ />
)} -
{isDesktop() && (
@@ -143,52 +135,6 @@ const MyAdsTable = () => {
)} - - - - - - {my_ads_store.activate_deactivate_error_message} - - - - - - - - ); -}; - -CancelAddPaymentMethodModal.propTypes = { - is_floating: PropTypes.bool, -}; - -export default observer(CancelAddPaymentMethodModal); diff --git a/packages/p2p/src/components/my-profile/payment-methods/add-payment-method/select-payment-method.jsx b/packages/p2p/src/components/my-profile/payment-methods/add-payment-method/select-payment-method.jsx index 37acec080d83..f7ea14e4a03c 100644 --- a/packages/p2p/src/components/my-profile/payment-methods/add-payment-method/select-payment-method.jsx +++ b/packages/p2p/src/components/my-profile/payment-methods/add-payment-method/select-payment-method.jsx @@ -31,7 +31,7 @@ const SelectPaymentMethod = () => { label={localize('Payment method')} list_items={my_profile_store.payment_methods_list_items} onItemSelection={({ value }) => { - my_profile_store.setSelectedPaymentMethod(value); + setTimeout(() => my_profile_store.setSelectedPaymentMethod(value), 0); }} required trailing_icon={} diff --git a/packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/edit-payment-method-form.jsx b/packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/edit-payment-method-form.jsx index eff872701498..d0abeaa3dce9 100644 --- a/packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/edit-payment-method-form.jsx +++ b/packages/p2p/src/components/my-profile/payment-methods/payment-methods-list/edit-payment-method-form.jsx @@ -1,17 +1,18 @@ -import PropTypes from 'prop-types'; import classNames from 'classnames'; import React from 'react'; import { observer } from 'mobx-react-lite'; -import { Field, Form, Formik } from 'formik'; -import { Button, DesktopWrapper, Input, Loading, Modal, Text } from '@deriv/components'; +import { Field, Form } from 'formik'; +import { Button, DesktopWrapper, Input, Loading, Text } from '@deriv/components'; import { isDesktop, isMobile } from '@deriv/shared'; import { Localize, localize } from 'Components/i18next'; import { useStores } from 'Stores'; -import CancelEditPaymentMethodModal from './cancel-edit-payment-method-modal.jsx'; import PageReturn from 'Components/page-return/page-return.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import ModalForm from 'Components/modal-manager/modal-form'; -const EditPaymentMethodForm = ({ formik_ref }) => { +const EditPaymentMethodForm = () => { const { general_store, my_profile_store } = useStores(); + const { showModal } = useModalManagerContext(); React.useEffect(() => { return () => { @@ -27,10 +28,8 @@ const EditPaymentMethodForm = ({ formik_ref }) => { return ( - - { { if (dirty) { - my_profile_store.setIsCancelEditPaymentMethodModalOpen(true); + showModal({ + key: 'CancelEditPaymentMethodModal', + }); } else { my_profile_store.setShouldShowEditPaymentMethodForm(false); } @@ -114,7 +115,9 @@ const EditPaymentMethodForm = ({ formik_ref }) => { large onClick={() => { if (dirty) { - my_profile_store.setIsCancelEditPaymentMethodModalOpen(true); + showModal({ + key: 'CancelEditPaymentMethodModal', + }); } else { my_profile_store.setPaymentMethodToEdit(null); my_profile_store.setShouldShowEditPaymentMethodForm(false); @@ -137,34 +140,9 @@ const EditPaymentMethodForm = ({ formik_ref }) => { ); }} - - - - - {my_profile_store.add_payment_method_error_message} - - - - - ); } diff --git a/packages/p2p/src/components/order-details/order-details.jsx b/packages/p2p/src/components/order-details/order-details.jsx index 1f0f729f825f..2da26a6aa716 100644 --- a/packages/p2p/src/components/order-details/order-details.jsx +++ b/packages/p2p/src/components/order-details/order-details.jsx @@ -5,8 +5,6 @@ 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'; import OrderDetailsFooter from 'Components/order-details/order-details-footer.jsx'; @@ -19,15 +17,14 @@ import PaymentMethodAccordionHeader from './payment-method-accordion-header.jsx' import PaymentMethodAccordionContent from './payment-method-accordion-content.jsx'; import MyProfileSeparatorContainer from '../my-profile/my-profile-separator-container'; import { setDecimalPlaces, removeTrailingZeros, roundOffDecimal } from 'Utils/format-value'; -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'; -import './order-details.scss'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import 'Components/order-details/order-details.scss'; const OrderDetails = observer(() => { const { general_store, my_profile_store, order_store, sendbird_store } = useStores(); + const { hideModal, showModal, useRegisterModalProps } = useModalManagerContext(); const { account_currency, @@ -72,6 +69,12 @@ const OrderDetails = observer(() => { const rating_average_decimal = review_details ? Number(review_details.rating).toFixed(1) : undefined; + const showRatingModal = () => { + showModal({ + key: 'RatingModal', + }); + }; + React.useEffect(() => { const disposeListeners = sendbird_store.registerEventListeners(); const disposeReactions = sendbird_store.registerMobXReactions(); @@ -97,7 +100,7 @@ const OrderDetails = observer(() => { general_store.props.setP2POrderProps({ order_id: order_store.order_id, redirectToOrderDetails: general_store.redirectToOrderDetails, - setIsRatingModalOpen: order_store.setIsRatingModalOpen, + setIsRatingModalOpen: is_open => (is_open ? showRatingModal : hideModal), }); }; }, []); // eslint-disable-line react-hooks/exhaustive-deps @@ -110,6 +113,28 @@ const OrderDetails = observer(() => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [completion_time]); + useRegisterModalProps({ + key: 'RatingModal', + props: { + is_buy_order_for_user, + is_user_recommended_previously, + onClickDone: () => { + order_store.setOrderRating(id); + general_store.props.removeNotificationMessage({ + key: `order-${id}`, + }); + general_store.props.removeNotificationByKey({ + key: `order-${id}`, + }); + }, + onClickSkip: () => { + order_store.setRatingValue(0); + hideModal(); + }, + previous_recommendation, + }, + }); + if (sendbird_store.should_show_chat_on_orders) { return ; } @@ -121,28 +146,6 @@ const OrderDetails = observer(() => { return ( - {is_active_order && ( - order_store.setIsRecommended(null)} - onClickDone={() => { - order_store.setOrderRating(id); - general_store.props.removeNotificationMessage({ key: `order-${id}` }); - general_store.props.removeNotificationByKey({ key: `order-${id}` }); - }} - onClickNotRecommended={() => order_store.setIsRecommended(0)} - onClickRecommended={() => order_store.setIsRecommended(1)} - onClickSkip={() => { - order_store.setRatingValue(0); - order_store.setIsRatingModalOpen(false); - }} - onClickStar={order_store.handleRating} - previous_recommendation={previous_recommendation} - rating_value={order_store.rating_value} - /> - )} {should_show_lost_funds_banner && (
{ )} {!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)} - /> - )}
@@ -319,26 +302,6 @@ const OrderDetails = observer(() => { )} {is_completed_order && !review_details && ( - order_store.setIsRecommended(null)} - onClickDone={() => { - order_store.setOrderRating(id); - general_store.props.removeNotificationMessage({ key: `order-${id}` }); - general_store.props.removeNotificationByKey({ key: `order-${id}` }); - }} - onClickNotRecommended={() => order_store.setIsRecommended(0)} - onClickRecommended={() => order_store.setIsRecommended(1)} - onClickSkip={() => { - order_store.setRatingValue(0); - order_store.setIsRatingModalOpen(false); - }} - onClickStar={order_store.handleRating} - previous_recommendation={previous_recommendation} - rating_value={order_store.rating_value} - />
{ } is_disabled={!is_reviewable} large - onClick={() => order_store.setIsRatingModalOpen(true)} + onClick={showRatingModal} />
diff --git a/packages/p2p/src/components/orders/order-table/order-table-row.jsx b/packages/p2p/src/components/orders/order-table/order-table-row.jsx index 8a0a57784baf..fe076edad526 100644 --- a/packages/p2p/src/components/orders/order-table/order-table-row.jsx +++ b/packages/p2p/src/components/orders/order-table/order-table-row.jsx @@ -10,7 +10,7 @@ import { DesktopWrapper, Icon, MobileWrapper, Table, Text } from '@deriv/compone import { formatMoney } from '@deriv/shared'; import { localize } from 'Components/i18next'; import RatingCellRenderer from 'Components/rating-cell-renderer'; -import RatingModal from 'Components/rating-modal'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const Title = ({ send_amount, currency, order_purchase_datetime, order_type }) => { return ( @@ -37,7 +37,7 @@ const OrderRow = ({ row: order }) => { const [order_state, setOrderState] = React.useState(order); // Use separate state to force refresh when (FE-)expired. const [is_timer_visible, setIsTimerVisible] = React.useState(); const should_show_order_details = React.useRef(true); - const [should_show_rating_modal, setShouldShowRatingModal] = React.useState(false); // Need a separate state to prevent re-render. DON'T REMOVE! + const { showModal, hideModal } = useModalManagerContext(); const { account_currency, @@ -81,6 +81,32 @@ const OrderRow = ({ row: order }) => { return () => {}; }; + const showRatingModal = () => { + showModal({ + key: 'RatingModal', + props: { + is_buy_order_for_user, + is_user_recommended_previously, + onClickDone: () => { + order_store.setOrderRating(id); + hideModal(); + should_show_order_details.current = true; + order_store.setRatingValue(0); + general_store.props.removeNotificationMessage({ key: `order-${id}` }); + general_store.props.removeNotificationByKey({ key: `order-${id}` }); + order_store.setIsLoading(true); + order_store.setOrders([]); + order_store.loadMoreOrders({ startIndex: 0 }); + }, + onClickSkip: () => { + order_store.setRatingValue(0); + hideModal(); + should_show_order_details.current = true; + }, + }, + }); + }; + React.useEffect(() => { const countDownTimer = () => { const { distance, label } = getTimeLeft(order_expiry_milliseconds); @@ -103,32 +129,6 @@ const OrderRow = ({ row: order }) => { return ( - order_store.setIsRecommended(null)} - onClickDone={() => { - order_store.setOrderRating(id); - setShouldShowRatingModal(false); - should_show_order_details.current = true; - order_store.setRatingValue(0); - general_store.props.removeNotificationMessage({ key: `order-${id}` }); - general_store.props.removeNotificationByKey({ key: `order-${id}` }); - order_store.setIsLoading(true); - order_store.setOrders([]); - order_store.loadMoreOrders({ startIndex: 0 }); - }} - onClickNotRecommended={() => order_store.setIsRecommended(0)} - onClickRecommended={() => order_store.setIsRecommended(1)} - onClickSkip={() => { - order_store.setRatingValue(0); - setShouldShowRatingModal(false); - should_show_order_details.current = true; - }} - onClickStar={order_store.handleRating} - rating_value={order_store.rating_value} - />
{ rating={rating} onClickUserRatingButton={() => { should_show_order_details.current = false; - setShouldShowRatingModal(true); + showRatingModal(); }} /> ) @@ -235,7 +235,7 @@ const OrderRow = ({ row: order }) => { rating={rating} onClickUserRatingButton={() => { should_show_order_details.current = false; - setShouldShowRatingModal(true); + showRatingModal(); }} /> )} diff --git a/packages/p2p/src/components/recommended-by/recommended-by.jsx b/packages/p2p/src/components/recommended-by/recommended-by.jsx index 6b5697068d7e..0a93c1f2aae8 100644 --- a/packages/p2p/src/components/recommended-by/recommended-by.jsx +++ b/packages/p2p/src/components/recommended-by/recommended-by.jsx @@ -3,10 +3,11 @@ import PropTypes from 'prop-types'; import { Icon, Popover, Text } from '@deriv/components'; import { isMobile } from '@deriv/shared'; import { localize } from 'Components/i18next'; -import RecommendedModal from './recommended-modal.jsx'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; const RecommendedBy = ({ recommended_average, recommended_count }) => { - const [is_recommended_modal_open, setIsRecommendedModalOpen] = React.useState(false); + const { showModal } = useModalManagerContext(); + const getRecommendedMessage = () => { if (recommended_count) { if (recommended_count === 1) { @@ -23,16 +24,21 @@ const RecommendedBy = ({ recommended_average, recommended_count }) => { return ( - setIsRecommendedModalOpen(true) : () => {}} + onClick={ + isMobile() + ? () => + showModal({ + key: 'RecommendedModal', + props: { + message: getRecommendedMessage(), + }, + }) + : () => {} + } > ( - - - - {message} - - - -