From 12512bab773b75a2158d352b017d20b7e0f5e948 Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Fri, 16 Feb 2024 15:43:41 +0400 Subject: [PATCH 01/17] feat: add unavailable button --- .../advertiser-page-adverts.jsx | 1 + .../advertiser-page/advertiser-page-row.jsx | 27 ++++-- .../pages/buy-sell/buy-sell-row-action.tsx | 86 +++++++++++++++++++ .../p2p/src/pages/buy-sell/buy-sell-row.jsx | 36 ++++---- .../p2p/src/pages/buy-sell/buy-sell-table.jsx | 1 + packages/p2p/src/types/adverts.types.ts | 2 + 6 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx diff --git a/packages/p2p/src/pages/advertiser-page/advertiser-page-adverts.jsx b/packages/p2p/src/pages/advertiser-page/advertiser-page-adverts.jsx index 752938469e38..a2b5831d8279 100644 --- a/packages/p2p/src/pages/advertiser-page/advertiser-page-adverts.jsx +++ b/packages/p2p/src/pages/advertiser-page/advertiser-page-adverts.jsx @@ -62,6 +62,7 @@ const AdvertiserPageAdverts = () => { {''} + )} diff --git a/packages/p2p/src/pages/advertiser-page/advertiser-page-row.jsx b/packages/p2p/src/pages/advertiser-page/advertiser-page-row.jsx index 461b088c98d0..10570de25291 100644 --- a/packages/p2p/src/pages/advertiser-page/advertiser-page-row.jsx +++ b/packages/p2p/src/pages/advertiser-page/advertiser-page-row.jsx @@ -1,12 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Button, Table, Text } from '@deriv/components'; +import { Table, Text } from '@deriv/components'; import { isMobile } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; import { useP2PExchangeRate } from '@deriv/hooks'; import { useStores } from 'Stores'; import { buy_sell } from 'Constants/buy-sell'; -import { localize, Localize } from 'Components/i18next'; +import { Localize } from 'Components/i18next'; +import BuySellRowAction from 'Pages/buy-sell/buy-sell-row-action'; import { generateEffectiveRate } from 'Utils/format-value'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import './advertiser-page-row.scss'; @@ -22,6 +23,8 @@ const AdvertiserPageRow = ({ row: advert }) => { } = useStore(); const { effective_rate, + eligibility_status, + is_eligible, local_currency, max_order_amount_limit_display, min_order_amount_limit_display, @@ -112,9 +115,13 @@ const AdvertiserPageRow = ({ row: advert }) => { ) : ( - + )} @@ -148,9 +155,13 @@ const AdvertiserPageRow = ({ row: advert }) => { ) : ( - + )} diff --git a/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx new file mode 100644 index 000000000000..a9d8677a4bb1 --- /dev/null +++ b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Button } from '@deriv/components'; +import { useStore } from '@deriv/stores'; +import { Localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import { useStores } from 'Stores'; + +type TBuySellRowActionProps = { + account_currency?: string; + className?: string; + eligibility_status: string[]; + is_buy_advert?: boolean; + is_eligible?: boolean; + onClick: () => void; +}; + +const BuySellRowAction = ({ + account_currency, + className, + eligibility_status, + is_buy_advert, + is_eligible, + onClick, +}: TBuySellRowActionProps) => { + const { showModal } = useModalManagerContext(); + const { + ui: { is_desktop }, + } = useStore(); + const { general_store } = useStores(); + + const getEligibilityStatus = () => { + if (eligibility_status?.length === 1) { + if (eligibility_status.includes('completion_rate')) { + return 'Your completion rate is too low for this ad.'; + } else if (eligibility_status.includes('join_date')) { + return "You've not used Deriv P2P long enough for this ad."; + } + } + + return "The advertiser has set conditions for this ad that you don't meet."; + }; + + const onUnavailableClick = () => { + showModal({ key: 'ErrorModal', props: { error_message: getEligibilityStatus() } }); + }; + + if (is_eligible) { + if (is_buy_advert) { + return ( + + ); + } + + return ( + + ); + } + + return ( + + ); +}; + +export default BuySellRowAction; diff --git a/packages/p2p/src/pages/buy-sell/buy-sell-row.jsx b/packages/p2p/src/pages/buy-sell/buy-sell-row.jsx index f1b9c83f0951..4aae71514358 100644 --- a/packages/p2p/src/pages/buy-sell/buy-sell-row.jsx +++ b/packages/p2p/src/pages/buy-sell/buy-sell-row.jsx @@ -3,18 +3,19 @@ import { useHistory } from 'react-router-dom'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import { Button, Icon, Table, Text } from '@deriv/components'; +import { Icon, Table, Text } from '@deriv/components'; import { isMobile, routes } from '@deriv/shared'; import { observer, useStore } from '@deriv/stores'; import { useP2PExchangeRate } from '@deriv/hooks'; -import { Localize, localize } from 'Components/i18next'; +import { Localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import { OnlineStatusAvatar } from 'Components/online-status'; import StarRating from 'Components/star-rating'; import TradeBadge from 'Components/trade-badge'; import { document_status_codes, identity_status_codes } from 'Constants/account-status-codes'; import { buy_sell } from 'Constants/buy-sell'; +import BuySellRowAction from 'Pages/buy-sell/buy-sell-row-action'; import { useStores } from 'Stores'; import { generateEffectiveRate } from 'Utils/format-value'; @@ -26,6 +27,8 @@ const BuySellRow = ({ row: advert }) => { advertiser_details, counterparty_type, effective_rate, + eligibility_status, + is_eligible, local_currency, max_order_amount_limit_display, min_order_amount_limit_display, @@ -187,19 +190,14 @@ const BuySellRow = ({ row: advert }) => { )} {!is_my_advert && ( - + /> )} @@ -293,11 +291,13 @@ const BuySellRow = ({ row: advert }) => { ) : ( - + )} diff --git a/packages/p2p/src/pages/buy-sell/buy-sell-table.jsx b/packages/p2p/src/pages/buy-sell/buy-sell-table.jsx index d11cda8044f3..264809491afc 100644 --- a/packages/p2p/src/pages/buy-sell/buy-sell-table.jsx +++ b/packages/p2p/src/pages/buy-sell/buy-sell-table.jsx @@ -107,6 +107,7 @@ const BuySellTable = ({ onScroll }) => { + )} diff --git a/packages/p2p/src/types/adverts.types.ts b/packages/p2p/src/types/adverts.types.ts index e94b1b8166cc..f87d045e709b 100644 --- a/packages/p2p/src/types/adverts.types.ts +++ b/packages/p2p/src/types/adverts.types.ts @@ -29,8 +29,10 @@ export type TAdvertProps = { description: string; effective_rate: number; effective_rate_display: string; + eligibility_status: string[]; id: string; is_active: number; + is_eligible: number; is_visible: number; local_currency: string; max_order_amount: number; From 2a2f2cbcfba83a0b5045c8c8bbcbcc85679d9988 Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Mon, 19 Feb 2024 12:04:04 +0400 Subject: [PATCH 02/17] fix: translation --- .../__tests__/buy-sell-row-action.spec.tsx | 82 +++++++++++++++++++ .../pages/buy-sell/buy-sell-row-action.tsx | 12 +-- 2 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 packages/p2p/src/pages/buy-sell/__tests__/buy-sell-row-action.spec.tsx diff --git a/packages/p2p/src/pages/buy-sell/__tests__/buy-sell-row-action.spec.tsx b/packages/p2p/src/pages/buy-sell/__tests__/buy-sell-row-action.spec.tsx new file mode 100644 index 000000000000..8e75e1001f5f --- /dev/null +++ b/packages/p2p/src/pages/buy-sell/__tests__/buy-sell-row-action.spec.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { mockStore, StoreProvider } from '@deriv/stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import BuySellRowAction from '../buy-sell-row-action'; + +const mock_modal_manager: DeepPartial> = { + showModal: jest.fn(), +}; + +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn(() => mock_modal_manager), +})); + +const wrapper = ({ children }: { children: JSX.Element }) => ( + {children} +); + +describe('', () => { + it('should render the component', () => { + render(, { wrapper }); + expect(screen.getByRole('button', { name: 'Unavailable' })).toBeInTheDocument(); + }); + it('should show the Buy button if advertiser is eligible to create a buy order against the advert', () => { + const onClick = jest.fn(); + + render(, { wrapper }); + + const buy_btn = screen.getByRole('button', { name: 'Buy USD' }); + expect(buy_btn).toBeInTheDocument(); + userEvent.click(buy_btn); + expect(onClick).toHaveBeenCalled(); + }); + it('should show the Sell button if advertiser is eligible to create a sell order against the advert', () => { + const onClick = jest.fn(); + + render(, { wrapper }); + + const sell_btn = screen.getByRole('button', { name: 'Sell USD' }); + expect(sell_btn).toBeInTheDocument(); + userEvent.click(sell_btn); + expect(onClick).toHaveBeenCalled(); + }); + it('should show the proper message if advertiser does not meet the completion rate', () => { + const onClick = jest.fn(); + const eligibility_status = ['completion_rate']; + + render(, { wrapper }); + userEvent.click(screen.getByRole('button', { name: 'Unavailable' })); + + expect(mock_modal_manager.showModal).toHaveBeenCalledWith({ + key: 'ErrorModal', + props: { error_message: 'Your completion rate is too low for this ad.' }, + }); + }); + it('should show the proper message if advertiser does not meet the minimum no. of joining days', () => { + const onClick = jest.fn(); + const eligibility_status = ['join_date']; + + render(, { wrapper }); + userEvent.click(screen.getByRole('button', { name: 'Unavailable' })); + + expect(mock_modal_manager.showModal).toHaveBeenCalledWith({ + key: 'ErrorModal', + props: { error_message: "You've not used Deriv P2P long enough for this ad." }, + }); + }); + it('should show the proper message if advertiser does not meet any of the conditions set for the advert', () => { + const onClick = jest.fn(); + const eligibility_status = ['join_date', 'completion_rate', 'country']; + + render(, { wrapper }); + userEvent.click(screen.getByRole('button', { name: 'Unavailable' })); + + expect(mock_modal_manager.showModal).toHaveBeenCalledWith({ + key: 'ErrorModal', + props: { error_message: "The advertiser has set conditions for this ad that you don't meet." }, + }); + }); +}); diff --git a/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx index a9d8677a4bb1..1bce2f370bda 100644 --- a/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx +++ b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx @@ -1,17 +1,17 @@ import React from 'react'; import { Button } from '@deriv/components'; import { useStore } from '@deriv/stores'; -import { Localize } from 'Components/i18next'; +import { Localize, localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import { useStores } from 'Stores'; type TBuySellRowActionProps = { account_currency?: string; className?: string; - eligibility_status: string[]; + eligibility_status?: string[]; is_buy_advert?: boolean; is_eligible?: boolean; - onClick: () => void; + onClick?: () => void; }; const BuySellRowAction = ({ @@ -31,13 +31,13 @@ const BuySellRowAction = ({ const getEligibilityStatus = () => { if (eligibility_status?.length === 1) { if (eligibility_status.includes('completion_rate')) { - return 'Your completion rate is too low for this ad.'; + return localize('Your completion rate is too low for this ad.'); } else if (eligibility_status.includes('join_date')) { - return "You've not used Deriv P2P long enough for this ad."; + return localize("You've not used Deriv P2P long enough for this ad."); } } - return "The advertiser has set conditions for this ad that you don't meet."; + return localize("The advertiser has set conditions for this ad that you don't meet."); }; const onUnavailableClick = () => { From aca4d2e1e00c3f94caa6dcd8abf88a11edbe6be0 Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Wed, 6 Mar 2024 11:27:00 +0400 Subject: [PATCH 03/17] feat: ad terms flow --- .../block-selector/block-selector.scss | 26 ++ .../block-selector/block-selector.tsx | 72 ++++ .../src/components/block-selector/index.ts | 4 + .../__tests__/ad-cancel-modal.spec.tsx | 62 +++ .../ad-cancel-modal.tsx} | 19 +- .../modals/ad-cancel-modal/index.ts | 3 + .../__tests__/edit-ad-cancel-modal.spec.tsx | 55 --- .../modals/edit-ad-cancel-modal/index.ts | 3 - .../modals/preferred-countries-modal/index.ts | 4 + .../preferred-countries-modal.scss | 25 ++ .../preferred-countries-modal.tsx | 56 +++ packages/p2p/src/constants/modals.ts | 15 +- .../ad-conditions-section.scss | 7 + .../ad-conditions-section.tsx | 89 +++++ .../my-ads/ad-conditions-section/index.ts | 4 + .../ad-form-controller.scss | 8 + .../ad-form-controller/ad-form-controller.tsx | 69 ++++ .../pages/my-ads/ad-form-controller/index.ts | 4 + .../ad-payment-details-section.scss | 8 + .../ad-payment-details-section.tsx | 51 +++ .../ad-payment-details-section/index.ts | 4 + .../ad-type-section/ad-type-section.scss | 106 ++++++ .../ad-type-section/ad-type-section.tsx | 308 +++++++++++++++ .../src/pages/my-ads/ad-type-section/index.ts | 4 + .../my-ads/buy-ad-payment-methods-list.jsx | 2 +- .../my-ads/create-ad-form-payment-methods.jsx | 13 +- .../p2p/src/pages/my-ads/create-ad-form.jsx | 360 ++---------------- .../p2p/src/pages/my-ads/create-ad-form.scss | 154 ++------ packages/p2p/src/pages/my-ads/create-ad.jsx | 7 - .../my-ads/edit-ad-form-payment-methods.jsx | 9 - .../p2p/src/pages/my-ads/edit-ad-form.jsx | 346 ++--------------- .../p2p/src/pages/my-ads/edit-ad-form.scss | 3 - .../my-ads/sell-ad-payment-methods-list.scss | 1 + packages/p2p/src/stores/my-ads-store.js | 23 +- 34 files changed, 1035 insertions(+), 889 deletions(-) create mode 100644 packages/p2p/src/components/block-selector/block-selector.scss create mode 100644 packages/p2p/src/components/block-selector/block-selector.tsx create mode 100644 packages/p2p/src/components/block-selector/index.ts create mode 100644 packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx rename packages/p2p/src/components/modal-manager/modals/{edit-ad-cancel-modal/edit-ad-cancel-modal.tsx => ad-cancel-modal/ad-cancel-modal.tsx} (70%) create mode 100644 packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts delete mode 100644 packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/__tests__/edit-ad-cancel-modal.spec.tsx delete mode 100644 packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/index.ts create mode 100644 packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/index.ts create mode 100644 packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss create mode 100644 packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss create mode 100644 packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-conditions-section/index.ts create mode 100644 packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss create mode 100644 packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-form-controller/index.ts create mode 100644 packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.scss create mode 100644 packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-payment-details-section/index.ts create mode 100644 packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss create mode 100644 packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-type-section/index.ts diff --git a/packages/p2p/src/components/block-selector/block-selector.scss b/packages/p2p/src/components/block-selector/block-selector.scss new file mode 100644 index 000000000000..043d3a0f57be --- /dev/null +++ b/packages/p2p/src/components/block-selector/block-selector.scss @@ -0,0 +1,26 @@ +.block-selector { + margin: 2rem 0; + + &__label { + display: flex; + align-items: center; + column-gap: 1rem; + } + + &__options { + display: flex; + column-gap: 1rem; + } + + &__option { + background-color: transparent; + border: 1px solid var(--border-normal); + border-radius: 0.4rem; + padding: 0.7rem 1.6rem; + width: 12.4rem; + + &--selected { + background-color: var(--general-main-3); + } + } +} diff --git a/packages/p2p/src/components/block-selector/block-selector.tsx b/packages/p2p/src/components/block-selector/block-selector.tsx new file mode 100644 index 000000000000..04ccdfa7fd18 --- /dev/null +++ b/packages/p2p/src/components/block-selector/block-selector.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Icon, Popover, Text } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +type TBlockSelectorOptionProps = { + is_selected?: boolean; + name: string; + value: number; +}; +type TBlockSelectorProps = { + label: string; + onSelect: (value: number) => void; + options: TBlockSelectorOptionProps[]; + tooltip_info: string; + value: number; +}; + +const BlockSelector = ({ label, onSelect, options, tooltip_info, value }: TBlockSelectorProps) => { + const [selectors, setSelectors] = React.useState(options); + const [selected_value, setSelectedValue] = React.useState(value); + const onClick = e => { + const selected_value = e.target.getAttribute('data-value'); + onSelect?.(0); + setSelectedValue(0); + + const updated_selectors = selectors.map(selector => { + const is_selected = !selector.is_selected && selector.value == selected_value; + + if (is_selected) { + onSelect?.(selector.value); + setSelectedValue(selector.value); + } + + return { ...selector, is_selected }; + }); + + setSelectors(updated_selectors); + }; + + return ( +
+
+ + + + }> + + +
+
+ {selectors.map(option => ( + + {option.name} + + ))} +
+
+ ); +}; + +export default BlockSelector; diff --git a/packages/p2p/src/components/block-selector/index.ts b/packages/p2p/src/components/block-selector/index.ts new file mode 100644 index 000000000000..c4c826ede3f1 --- /dev/null +++ b/packages/p2p/src/components/block-selector/index.ts @@ -0,0 +1,4 @@ +import BlockSelector from './block-selector'; +import './block-selector.scss'; + +export default BlockSelector; diff --git a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx new file mode 100644 index 000000000000..71a644d0ce41 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import AdCancelModal from '../ad-cancel-modal'; + +const mock_modal_manager = { + hideModal: jest.fn(), + is_modal_open: true, +}; + +jest.mock('Components/modal-manager/modal-manager-context'); +const mocked_useModalManagerContext = useModalManagerContext as jest.MockedFunction< + () => Partial> +>; + +mocked_useModalManagerContext.mockImplementation(() => mock_modal_manager); + +describe('', () => { + let modal_root_el: HTMLElement; + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + it('should render the AdCancelModal', () => { + const message = "If you choose to cancel, the details you've entered will be lost."; + const onConfirm = jest.fn(); + const title = 'Cancel ad creation?'; + render(); + + expect( + screen.getByText("If you choose to cancel, the details you've entered will be lost.") + ).toBeInTheDocument(); + expect(screen.getByText('Cancel ad creation?')).toBeInTheDocument(); + }); + it("should close modal on clicking Don't cancel button", () => { + const message = 'If you choose to cancel, the edited details will be lost.'; + const title = 'Cancel your edits?'; + + render(); + + const dont_cancel_button = screen.getByText("Don't cancel"); + userEvent.click(dont_cancel_button); + expect(mock_modal_manager.hideModal).toHaveBeenCalled(); + }); + it('should call onConfirm on clicking Cancel button', () => { + const message = 'If you choose to cancel, the edited details will be lost.'; + const onConfirm = jest.fn(); + const title = 'Cancel your edits?'; + + render(); + + const cancel_button = screen.getByText('Cancel'); + userEvent.click(cancel_button); + expect(onConfirm).toHaveBeenCalled(); + }); +}); diff --git a/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/edit-ad-cancel-modal.tsx b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx similarity index 70% rename from packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/edit-ad-cancel-modal.tsx rename to packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx index 01dd379ac1db..ffe442c0cca8 100644 --- a/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/edit-ad-cancel-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx @@ -2,18 +2,21 @@ import React from 'react'; import { Button, Modal, Text } from '@deriv/components'; import { localize, Localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; -import { useStores } from 'Stores'; -const EditAdCancelModal = () => { - const { my_ads_store } = useStores(); +type TAdCancelModalProps = { + message: string; + onConfirm?: () => void; + title: string; +}; + +const AdCancelModal = ({ message, onConfirm, title }: TAdCancelModalProps) => { const { hideModal, is_modal_open } = useModalManagerContext(); - const { setShowEditAdForm } = my_ads_store; return ( - + - + @@ -22,7 +25,7 @@ const EditAdCancelModal = () => { text={localize('Cancel')} onClick={() => { hideModal(); - setShowEditAdForm(false); + onConfirm?.(); }} secondary large @@ -33,4 +36,4 @@ const EditAdCancelModal = () => { ); }; -export default EditAdCancelModal; +export default AdCancelModal; diff --git a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts new file mode 100644 index 000000000000..e18602925603 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts @@ -0,0 +1,3 @@ +import AdCancelModal from './ad-cancel-modal'; + +export default AdCancelModal; diff --git a/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/__tests__/edit-ad-cancel-modal.spec.tsx b/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/__tests__/edit-ad-cancel-modal.spec.tsx deleted file mode 100644 index de81d4fb8e10..000000000000 --- a/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/__tests__/edit-ad-cancel-modal.spec.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import userEvent from '@testing-library/user-event'; -import { render, screen } from '@testing-library/react'; -import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; -import { useStores } from 'Stores'; -import EditAdCancelModal from '../edit-ad-cancel-modal'; - -const mock_store: DeepPartial> = { - my_ads_store: { - setShowEditAdForm: jest.fn(), - }, -}; - -jest.mock('Stores', () => ({ - ...jest.requireActual('Stores'), - useStores: () => mock_store, -})); - -const mock_modal_manager = { - hideModal: jest.fn(), - is_modal_open: true, -}; - -jest.mock('Components/modal-manager/modal-manager-context'); -const mocked_useModalManagerContext = useModalManagerContext as jest.MockedFunction< - () => Partial> ->; - -mocked_useModalManagerContext.mockImplementation(() => mock_modal_manager); - -describe('', () => { - let modal_root_el: HTMLElement; - beforeAll(() => { - modal_root_el = document.createElement('div'); - modal_root_el.setAttribute('id', 'modal_root'); - document.body.appendChild(modal_root_el); - }); - - afterAll(() => { - document.body.removeChild(modal_root_el); - }); - it('should render the EditAdCancelModal', () => { - render(); - - expect(screen.getByText('Cancel your edits?')).toBeInTheDocument(); - }); - it('should close modal on clicking cancel button', () => { - render(); - - const cancel_button = screen.getByText('Cancel'); - userEvent.click(cancel_button); - expect(mock_modal_manager.hideModal).toBeCalledTimes(1); - expect(mock_store.my_ads_store.setShowEditAdForm).toBeCalledTimes(1); - }); -}); diff --git a/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/index.ts b/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/index.ts deleted file mode 100644 index 85a95c611255..000000000000 --- a/packages/p2p/src/components/modal-manager/modals/edit-ad-cancel-modal/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import EditAdCancelModal from './edit-ad-cancel-modal'; - -export default EditAdCancelModal; diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/index.ts b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/index.ts new file mode 100644 index 000000000000..7b1de568c588 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/index.ts @@ -0,0 +1,4 @@ +import PreferredCountriesModal from './preferred-countries-modal'; +import './preferred-countries-modal.scss'; + +export default PreferredCountriesModal; diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss new file mode 100644 index 000000000000..65bc41ed87af --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss @@ -0,0 +1,25 @@ +.preferred-countries-modal { + &__search-field { + margin-bottom: 0; + + input { + margin-left: 2.5rem; + } + } + + &__checkbox { + padding: 1.6rem 1rem; + } +} + +.dc-modal__container_preferred-countries-modal { + .dc-modal-body { + padding: 0; + } + + .dc-modal-footer { + > button { + flex: 1; + } + } +} diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx new file mode 100644 index 000000000000..0db0dfdbbba9 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Field, Form, Formik, FormikValues } from 'formik'; +import { Button, Icon, Input, Modal } from '@deriv/components'; +import { localize, Localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; + +type tPreferredCountriesModal = { + onApply?: () => void; +}; + +const PreferredCountriesModal = ({ onApply }: tPreferredCountriesModal) => { + const { hideModal, is_modal_open } = useModalManagerContext(); + + return ( + hideModal()} + > + + + {({ submitForm, values: { search } }) => ( +
+ + {({ field }: FormikValues) => ( + } + name='search' + onFocus={submitForm} + placeholder={localize('Search countries')} + trailing_icon={search ? : null} + type='text' + /> + )} + +
+ )} +
+
+ + + + +
+ ); +}; +export default PreferredCountriesModal; diff --git a/packages/p2p/src/constants/modals.ts b/packages/p2p/src/constants/modals.ts index 6c6f84f155ee..90ff4ee29955 100644 --- a/packages/p2p/src/constants/modals.ts +++ b/packages/p2p/src/constants/modals.ts @@ -1,6 +1,9 @@ import React from 'react'; export const Modals = { + AdCancelModal: React.lazy( + () => import(/* webpackChunkName: "ad-cancel-modal" */ 'Components/modal-manager/modals/ad-cancel-modal') + ), AdCreateEditErrorModal: React.lazy( () => import( @@ -85,12 +88,6 @@ export const Modals = { DisclaimerModal: React.lazy( () => import(/* webpackChunkName: "disclaimer-modal" */ 'Components/modal-manager/modals/disclaimer-modal') ), - EditAdCancelModal: React.lazy( - () => - import( - /* webpackChunkName: "edit-ad-cancel-modal" */ 'Components/modal-manager/modals/edit-ad-cancel-modal' - ) - ), EmailLinkBlockedModal: React.lazy( () => import( @@ -182,6 +179,12 @@ export const Modals = { /* webpackChunkName: "order-time-tooltip-modal" */ 'Components/modal-manager/modals/order-time-tooltip-modal' ) ), + PreferredCountriesModal: React.lazy( + () => + import( + /* webpackChunkName: "preferred-countries-modal" */ 'Components/modal-manager/modals/preferred-countries-modal' + ) + ), QuickAddModal: React.lazy( () => import(/* webpackChunkName: "quick-add-modal" */ 'Components/modal-manager/modals/quick-add-modal') ), diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss new file mode 100644 index 000000000000..03b10159e711 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss @@ -0,0 +1,7 @@ +.ad-conditions-section { + &__countries-label { + display: flex; + align-items: center; + column-gap: 1rem; + } +} diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx new file mode 100644 index 000000000000..c9b91249bc51 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { FormikValues, useFormikContext } from 'formik'; +import { Button, Icon, Popover, Text } from '@deriv/components'; +import BlockSelector from 'Components/block-selector'; +import { Localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import AdFormController from 'Pages/my-ads/ad-form-controller'; +import { useStores } from 'Stores'; +import CreateAdSummary from '../create-ad-summary.jsx'; + +const AdConditionsSection = ({ ...props }) => { + const { errors, isSubmitting, isValid, values } = useFormikContext(); + const { showModal } = useModalManagerContext(); + const { my_ads_store } = useStores(); + const { min_completion_rate, min_join_days } = my_ads_store.p2p_advert_information; + const joining_days = [ + { name: '15 days', value: 15 }, + { name: '30 days', value: 30 }, + { name: '60 days', value: 60 }, + ]; + const completion_rates = [ + { name: '50%', value: 50 }, + { name: '70%', value: 70 }, + { name: '90%', value: 90 }, + ]; + const setJoiningDays = (value: number) => { + my_ads_store.setMinJoinDays(value); + }; + const setMinCompletionRate = (value: number) => { + my_ads_store.setMinCompletionRate(value); + }; + + return ( + <> + + + + + + + + + +
+ + + + + } + > + + +
+ + + + ); +}; + +export default AdConditionsSection; diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/index.ts b/packages/p2p/src/pages/my-ads/ad-conditions-section/index.ts new file mode 100644 index 000000000000..61f9f8a01703 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/index.ts @@ -0,0 +1,4 @@ +import AdConditionsSection from './ad-conditions-section'; +import './ad-conditions-section.scss'; + +export default AdConditionsSection; diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss new file mode 100644 index 000000000000..d8bb545fb681 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss @@ -0,0 +1,8 @@ +.ad-form-controller { + display: flex; + justify-content: flex-end; + column-gap: 0.8rem; + margin-top: 10rem; + border-top: 1px solid var(--general-section-1); + padding: 2rem 0; +} diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx new file mode 100644 index 000000000000..b2aac2d09c1b --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { Button } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +type TAdFormControllerProps = { + action: string; + getCurrentStep: () => number; + getTotalSteps: () => number; + goToFirstStep: () => void; + goToStep: () => void; + goToLastStep: () => void; + goToNextStep: () => void; + goToPreviousStep: () => void; + is_next_btn_disabled: boolean; + onCancel: () => void; +}; + +const AdFormController = ({ + getCurrentStep, + getTotalSteps, + goToFirstStep, + goToStep, + goToLastStep, + goToNextStep, + goToPreviousStep, + is_next_btn_disabled, + onCancel, +}: TAdFormControllerProps) => { + return ( +
+ {getCurrentStep() === 1 && ( + + )} + {getCurrentStep() > 1 && ( + + )} + + {getCurrentStep() < getTotalSteps() ? ( + + ) : ( + + )} +
+ ); +}; + +export default AdFormController; diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/index.ts b/packages/p2p/src/pages/my-ads/ad-form-controller/index.ts new file mode 100644 index 000000000000..2fb5cde8b695 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/index.ts @@ -0,0 +1,4 @@ +import AdFormController from './ad-form-controller'; +import './ad-form-controller.scss'; + +export default AdFormController; diff --git a/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.scss b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.scss new file mode 100644 index 000000000000..22169a254910 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.scss @@ -0,0 +1,8 @@ +.ad-payment-details-section { + &__label { + display: flex; + flex-direction: column; + margin-top: 3.2rem; + padding-bottom: 1rem; + } +} diff --git a/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx new file mode 100644 index 000000000000..2306f7968156 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Field, FormikValues, useFormikContext } from 'formik'; +import { Text } from '@deriv/components'; +import { observer } from '@deriv/stores'; +import { Localize } from 'Components/i18next'; +import { buy_sell } from 'Constants/buy-sell'; +import AdFormController from 'Pages/my-ads/ad-form-controller'; +import CreateAdFormPaymentMethods from '../create-ad-form-payment-methods.jsx'; +import CreateAdSummary from '../create-ad-summary.jsx'; +import OrderTimeSelection from '../order-time-selection'; + +const AdPaymentDetailsSection = ({ ...props }) => { + const { errors, isValid, values } = useFormikContext(); + const [selected_payment_methods, setSelectedPaymentMethods] = React.useState([]); + const [is_next_btn_disabled, setIsNextBtnDisabled] = React.useState(true); + const is_sell_advert = values.type === buy_sell.SELL; + + React.useEffect(() => { + setIsNextBtnDisabled(!isValid || !selected_payment_methods?.length); + }, [selected_payment_methods]); + + return ( + <> + + {({ field }) => } +
+ + + + + {is_sell_advert ? ( + + ) : ( + + )} + +
+ + + + ); +}; + +export default observer(AdPaymentDetailsSection); diff --git a/packages/p2p/src/pages/my-ads/ad-payment-details-section/index.ts b/packages/p2p/src/pages/my-ads/ad-payment-details-section/index.ts new file mode 100644 index 000000000000..7358bf810424 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-payment-details-section/index.ts @@ -0,0 +1,4 @@ +import AdPaymentDetailsSection from './ad-payment-details-section'; +import './ad-payment-details-section.scss'; + +export default AdPaymentDetailsSection; diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss new file mode 100644 index 000000000000..2631f0a49cf1 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss @@ -0,0 +1,106 @@ +.ad-type-section { + &__container { + display: flex; + height: 8rem; + justify-content: space-between; + width: 67.2rem; + + .dc-input { + margin-bottom: 2.1rem; + margin-top: 2.1rem; + + &__container { + padding: 0 1rem; + } + &--hint { + margin-bottom: 0; + } + &__wrapper { + margin-top: 0; + } + } + + @include mobile { + flex-direction: column; + height: unset; + width: unset; + + .dc-input { + &__wrapper { + margin-bottom: 5rem; + } + } + } + } + + &__field { + height: 4rem; + margin-bottom: 0; + margin-left: 0; + width: 32.4rem; + + @include mobile { + width: inherit; + } + + &--contact-details { + margin-bottom: 5.9rem; + + @include mobile { + margin-bottom: 3.7rem; + } + } + &--single { + width: 18.9rem; + } + &--textarea { + height: 9.6rem; + margin-bottom: 0; + width: 67.2rem; + + @include mobile { + height: 9.8rem; + width: 90vw; + } + + .dc-input { + &__hint { + top: 9.7rem; + } + + &___counter { + top: 9.7rem; + right: 0rem; + + @include mobile { + display: flex; + right: 0; + } + } + + &__textarea { + padding-top: 1rem; + padding-left: 0; + } + + &__container { + height: 9.6rem; + } + } + } + } + + &__radio-group { + .dc-radio-group__circle { + border: 2px solid var(--border-hover); + + &--selected { + border: 4px solid var(--brand-red-coral); + } + } + + @include mobile { + padding: 0.5rem 0; + } + } +} diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx new file mode 100644 index 000000000000..fef3f7433a34 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx @@ -0,0 +1,308 @@ +import React from 'react'; +import { Field, FormikValues, useFormikContext } from 'formik'; +import { Input, RadioGroup, Text } from '@deriv/components'; +import { formatMoney } from '@deriv/shared'; +import { useStore } from '@deriv/stores'; +import FloatingRate from 'Components/floating-rate'; +import { localize, Localize } from 'Components/i18next'; +import { buy_sell } from 'Constants/buy-sell'; +import { ad_type } from 'Constants/floating-rate'; +import AdFormController from 'Pages/my-ads/ad-form-controller'; +import { useStores } from 'Stores'; + +type AdTypeSection = { + action?: string; +}; + +const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { + const { + client: { currency, local_currency_config }, + ui: is_desktop, + } = useStore(); + const local_currency = local_currency_config.currency; + const { buy_sell_store, floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); + const [selected_methods, setSelectedMethods] = React.useState([]); + const { dirty, errors, handleChange, isValid, setFieldValue, setFieldTouched, touched, values } = + useFormikContext(); + const { payment_method_details, payment_method_names, type } = my_ads_store.p2p_advert_information; + const is_buy_advert = type === buy_sell.BUY; + const is_edit = action === 'edit'; + const is_next_btn_disabled = is_edit ? !isValid : !dirty || !isValid; + const onChangeAdTypeHandler = (user_input: string) => { + if (floating_rate_store.rate_type === ad_type.FLOAT) { + if (user_input === buy_sell.SELL) { + setFieldValue('rate_type', '+0.01'); + } else { + setFieldValue('rate_type', '-0.01'); + } + } + + setFieldValue('type', user_input); + }; + + const onCancel = () => { + if (is_edit) { + const payment_methods_changed = is_buy_advert + ? !( + !!payment_method_names && + selected_methods?.every(pm => { + const method = my_profile_store.getPaymentMethodDisplayName(pm); + return payment_method_names.includes(method); + }) && + selected_methods.length === payment_method_names.length + ) + : !( + !!payment_method_details && + selected_methods.every(pm => Object.keys(payment_method_details).includes(pm)) && + selected_methods.length === Object.keys(payment_method_details).length + ); + if (dirty || payment_methods_changed) { + general_store.showModal({ + key: 'AdCancelModal', + props: { + message: 'If you choose to cancel, the edited details will be lost.', + onConfirm: () => { + my_ads_store.setShowEditAdForm(false); + }, + title: 'Cancel your edits?', + }, + }); + } else { + my_ads_store.setShowEditAdForm(false); + } + } else { + general_store.showModal({ + key: 'AdCancelModal', + props: { + message: "If you choose to cancel, the details you've entered will be lost.", + onConfirm: () => { + my_ads_store.setApiErrorMessage(''); + floating_rate_store.setApiErrorMessage(''); + my_ads_store.setShowAdForm(false); + buy_sell_store.setCreateSellAdFromNoAds(false); + }, + title: 'Cancel ad creation?', + }, + }); + } + }; + + return ( + <> + {!is_edit && ( + + {({ field }) => ( + onChangeAdTypeHandler(event.target.value)} + selected={values.type} + required + > + + + + )} + + )} +
+ + {({ field }) => ( + + {currency} + + } + onFocus={() => setFieldTouched('offer_amount', true)} + onChange={e => { + my_ads_store.restrictLength(e, handleChange); + }} + hint={ + values.type !== buy_sell.SELL || general_store.advertiser_info.balance_available == null + ? undefined + : localize('Your Deriv P2P balance is {{ dp2p_balance }}', { + dp2p_balance: `${formatMoney( + currency, + general_store.advertiser_info.balance_available, + true + )} ${currency}`, + }) + } + is_relative_hint + /> + )} + + + {({ field }) => + floating_rate_store.rate_type === ad_type.FLOAT ? ( + setFieldTouched('rate_type', true)} + offset={{ + upper_limit: parseInt(floating_rate_store.float_rate_offset_limit), + lower_limit: parseInt(floating_rate_store.float_rate_offset_limit) * -1, + }} + required + change_handler={e => { + my_ads_store.restrictDecimalPlace(e, handleChange); + }} + {...field} + /> + ) : ( + + {local_currency} + + } + onChange={e => { + my_ads_store.restrictLength(e, handleChange); + }} + onFocus={() => setFieldTouched('rate_type', true)} + required + /> + ) + } + +
+
+ + {({ field }) => ( + + {currency} + + } + onChange={e => { + my_ads_store.restrictLength(e, handleChange); + }} + onFocus={() => setFieldTouched('min_transaction', true)} + required + /> + )} + + + {({ field }) => ( + + {currency} + + } + onChange={e => { + my_ads_store.restrictLength(e, handleChange); + }} + onFocus={() => setFieldTouched('max_transaction', true)} + required + /> + )} + +
+ {values.type === buy_sell.SELL && ( +
+ + {({ field }) => ( + + + + } + error={touched.contact_info && errors.contact_info} + className='ad-type-section__field ad-type-section__field--textarea' + initial_character_count={general_store.contact_info.length} + required + has_character_counter + max_characters={300} + onFocus={() => setFieldTouched('contact_info', true)} + /> + )} + +
+ )} + + {({ field }) => ( + + + + } + hint={localize('This information will be visible to everyone.')} + className='ad-type-section__field ad-type-section__field--textarea' + initial_character_count={general_store.default_advert_description.length} + has_character_counter + max_characters={300} + onFocus={() => setFieldTouched('default_advert_description', true)} + required + /> + )} + + + + ); +}; + +export default AdTypeSection; diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/index.ts b/packages/p2p/src/pages/my-ads/ad-type-section/index.ts new file mode 100644 index 000000000000..4a7f24b21793 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-type-section/index.ts @@ -0,0 +1,4 @@ +import AdTypeSection from './ad-type-section'; +import './ad-type-section.scss'; + +export default AdTypeSection; diff --git a/packages/p2p/src/pages/my-ads/buy-ad-payment-methods-list.jsx b/packages/p2p/src/pages/my-ads/buy-ad-payment-methods-list.jsx index f73f19adca6e..aa4df18f6a99 100644 --- a/packages/p2p/src/pages/my-ads/buy-ad-payment-methods-list.jsx +++ b/packages/p2p/src/pages/my-ads/buy-ad-payment-methods-list.jsx @@ -102,8 +102,8 @@ const BuyAdPaymentMethodsList = ({ setShowList(false); setHideList(true); } else if (my_ads_store.payment_method_names.length < MAX_PAYMENT_METHOD_SELECTION) { - my_ads_store.payment_method_names.push(value); setSelectedMethods([...selected_methods, value]); + my_ads_store.payment_method_names.push(value); setPaymentMethodsList(payment_methods_list.filter(payment_method => payment_method.value !== value)); } if (typeof touched === 'function') touched(true); diff --git a/packages/p2p/src/pages/my-ads/create-ad-form-payment-methods.jsx b/packages/p2p/src/pages/my-ads/create-ad-form-payment-methods.jsx index 07521913768d..b6c3fe1080e3 100755 --- a/packages/p2p/src/pages/my-ads/create-ad-form-payment-methods.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad-form-payment-methods.jsx @@ -12,8 +12,8 @@ const CreateAdFormPaymentMethods = ({ is_sell_advert, onSelectPaymentMethods }) const { showModal } = useModalManagerContext(); const { data: p2p_advertiser_payment_methods } = useP2PAdvertiserPaymentMethods(); const { my_ads_store } = useStores(); - const [selected_buy_methods, setSelectedBuyMethods] = React.useState([]); - const [selected_sell_methods, setSelectedSellMethods] = React.useState([]); + const [selected_buy_methods, setSelectedBuyMethods] = React.useState(my_ads_store.payment_method_names); + const [selected_sell_methods, setSelectedSellMethods] = React.useState(my_ads_store.payment_method_ids); const onClickPaymentMethodCard = payment_method => { if (!my_ads_store.payment_method_ids.includes(payment_method.id)) { @@ -29,15 +29,6 @@ const CreateAdFormPaymentMethods = ({ is_sell_advert, onSelectPaymentMethods }) } }; - React.useEffect(() => { - return () => { - my_ads_store.payment_method_ids = []; - my_ads_store.payment_method_names = []; - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - React.useEffect(() => { if (is_sell_advert) { onSelectPaymentMethods(selected_sell_methods); diff --git a/packages/p2p/src/pages/my-ads/create-ad-form.jsx b/packages/p2p/src/pages/my-ads/create-ad-form.jsx index 8900f8be2ad2..58c393e9608a 100644 --- a/packages/p2p/src/pages/my-ads/create-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad-form.jsx @@ -1,30 +1,27 @@ import * as React from 'react'; -import classNames from 'classnames'; -import { Formik, Field, Form } from 'formik'; +import { Formik, Form } from 'formik'; import { Button, Checkbox, Div100vhContainer, - Input, + FormProgress, Modal, - RadioGroup, Text, ThemedScrollbars, + Wizard, } from '@deriv/components'; -import { formatMoney, isDesktop, isMobile } from '@deriv/shared'; -import { observer, useStore } from '@deriv/stores'; -import { useP2PExchangeRate } from '@deriv/hooks'; +import { isMobile } from '@deriv/shared'; +import { observer } from '@deriv/stores'; import { reaction } from 'mobx'; -import FloatingRate from 'Components/floating-rate'; import { Localize, localize } from 'Components/i18next'; 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 CreateAdFormPaymentMethods from './create-ad-form-payment-methods.jsx'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import { api_error_codes } from 'Constants/api-error-codes'; -import OrderTimeSelection from './order-time-selection'; +import AdConditionsSection from './ad-conditions-section'; +import AdPaymentDetailsSection from './ad-payment-details-section'; +import AdTypeSection from './ad-type-section'; import './create-ad-form.scss'; const CreateAdFormWrapper = ({ children }) => { @@ -35,22 +32,15 @@ const CreateAdFormWrapper = ({ children }) => { }; const CreateAdForm = () => { - const { - client: { currency, local_currency_config }, - } = useStore(); - const { buy_sell_store, floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); - const should_not_show_auto_archive_message_again = React.useRef(false); - const [selected_methods, setSelectedMethods] = React.useState([]); + const [current_step, setCurrentStep] = React.useState(0); const { useRegisterModalProps } = useModalManagerContext(); - const local_currency = local_currency_config.currency; - const exchange_rate = useP2PExchangeRate(local_currency); - - // eslint-disable-next-line no-shadow - const handleSelectPaymentMethods = selected_methods => { - setSelectedMethods(selected_methods); - }; + const steps = [ + { header: { title: 'Set ad type and amount' } }, + { header: { title: 'Set payment details' } }, + { header: { title: 'Set ad conditions' } }, + ]; const onCheckboxChange = () => (should_not_show_auto_archive_message_again.current = !should_not_show_auto_archive_message_again.current); @@ -89,6 +79,8 @@ const CreateAdForm = () => { floating_rate_store.setApiErrorMessage(''); my_ads_store.setShowAdForm(false); buy_sell_store.setCreateSellAdFromNoAds(false); + my_ads_store.payment_method_ids = []; + my_ads_store.payment_method_names = []; }; React.useEffect(() => { @@ -142,21 +134,7 @@ const CreateAdForm = () => { onSubmit={my_ads_store.handleSubmit} validate={my_ads_store.validateCreateAdForm} > - {({ errors, handleChange, isSubmitting, isValid, setFieldTouched, setFieldValue, touched, values }) => { - const is_sell_advert = values.type === buy_sell.SELL; - - const onChangeAdTypeHandler = user_input => { - if (floating_rate_store.rate_type === ad_type.FLOAT) { - if (user_input === buy_sell.SELL) { - setFieldValue('rate_type', '+0.01'); - } else { - setFieldValue('rate_type', '-0.01'); - } - } - - setFieldValue('type', user_input); - }; - + {() => { return (
@@ -165,300 +143,16 @@ const CreateAdForm = () => { is_scrollbar_hidden={isMobile()} > -
- - {({ field }) => ( - onChangeAdTypeHandler(event.target.value)} - selected={values.type} - required - > - - - - )} - - - -
- - {({ field }) => ( - - {currency} - - } - onFocus={() => setFieldTouched('offer_amount', true)} - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - hint={ - // Using two "==" is intentional as we're checking for nullish - // rather than falsy values. - !is_sell_advert || - general_store.advertiser_info.balance_available == null - ? undefined - : localize( - 'Your Deriv P2P balance is {{ dp2p_balance }}', - { - dp2p_balance: `${formatMoney( - currency, - general_store.advertiser_info - .balance_available, - true - )} ${currency}`, - } - ) - } - is_relative_hint - /> - )} - - - {({ field }) => - floating_rate_store.rate_type === ad_type.FLOAT ? ( - setFieldTouched('rate_type', true)} - offset={{ - upper_limit: parseInt( - floating_rate_store.float_rate_offset_limit - ), - lower_limit: - parseInt( - floating_rate_store.float_rate_offset_limit - ) * -1, - }} - required - change_handler={e => { - my_ads_store.restrictDecimalPlace(e, handleChange); - }} - {...field} - /> - ) : ( - - {local_currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('rate_type', true)} - required - /> - ) - } - -
-
- - {({ field }) => ( - - {currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('min_transaction', true)} - required - /> - )} - - - {({ field }) => ( - - {currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('max_transaction', true)} - required - /> - )} - -
- {is_sell_advert && ( -
- - {({ field }) => ( - - - - } - error={touched.contact_info && errors.contact_info} - className='create-ad-form__field create-ad-form__field--textarea' - initial_character_count={ - general_store.contact_info.length - } - required - has_character_counter - max_characters={300} - onFocus={() => setFieldTouched('contact_info', true)} - /> - )} - -
- )} - - {({ field }) => ( - - - - } - hint={localize('This information will be visible to everyone.')} - className='create-ad-form__field create-ad-form__field--textarea' - initial_character_count={ - general_store.default_advert_description.length - } - has_character_counter - max_characters={300} - onFocus={() => - setFieldTouched('default_advert_description', true) - } - required - /> - )} - - - {({ field }) => } - -
- - - - - {is_sell_advert ? ( - - ) : ( - - )} - -
- -
-
- - -
+ setCurrentStep(step.active_step - 1)} + nav={} + > + + + +
diff --git a/packages/p2p/src/pages/my-ads/create-ad-form.scss b/packages/p2p/src/pages/my-ads/create-ad-form.scss index e1d7dead6616..2d354e5d7d6a 100644 --- a/packages/p2p/src/pages/my-ads/create-ad-form.scss +++ b/packages/p2p/src/pages/my-ads/create-ad-form.scss @@ -10,19 +10,6 @@ } } - .dc-input__wrapper { - max-width: 67.2rem; // TODO: Kill these fixed widths. - margin-top: 2.1rem; - - @include mobile { - max-width: 90vw; - - .create-ad-form__field { - margin-bottom: 0; - } - } - } - &__label { &--focused { color: var(--text-prominent); @@ -35,31 +22,6 @@ max-width: 67.2rem; padding: 1.2rem 0; } - - &--text { - display: flex; - flex-direction: column; - margin-top: 3.2rem; - padding-bottom: 1rem; - } - } - - &__radio-group { - display: flex; - margin-top: unset; - padding-bottom: 1.2rem; - - .dc-radio-group__circle { - border: 2px solid var(--border-hover); - - &--selected { - border: 4px solid var(--brand-red-coral); - } - } - - @include mobile { - padding: 0.5rem 0; - } } &__scrollbar { @@ -94,97 +56,6 @@ } } - &__container { - display: flex; - height: 8rem; - justify-content: space-between; - width: 67.2rem; - - .dc-input { - margin-bottom: 2.1rem; - margin-top: 2.1rem; - - &__container { - padding: 0 1rem; - } - &--hint { - margin-bottom: 0; - } - &__wrapper { - margin-top: 0; - } - } - - @include mobile { - flex-direction: column; - height: unset; - width: unset; - - .dc-input { - &__wrapper { - margin-bottom: 5rem; - } - } - } - } - - &__field { - height: 4rem; - margin-bottom: 0; - margin-left: 0; - width: 32.4rem; - - @include mobile { - width: inherit; - } - - &--contact-details { - margin-bottom: 5.9rem; - - @include mobile { - margin-bottom: 3.7rem; - } - } - &--single { - width: 18.9rem; - } - &--textarea { - height: 9.6rem; - margin-bottom: 0; - width: 67.2rem; - - @include mobile { - height: 9.8rem; - width: 90vw; - } - - .dc-input { - &__hint { - top: 9.7rem; - } - - &___counter { - top: 9.7rem; - right: 0rem; - - @include mobile { - display: flex; - right: 0; - } - } - - &__textarea { - padding-top: 1rem; - padding-left: 0; - } - - &__container { - height: 9.6rem; - } - } - } - } - &__button { margin-left: 0.8rem; } @@ -194,6 +65,7 @@ background-color: var(--general-main-1); display: flex; justify-content: flex-end; + column-gap: 0.8rem; @include mobile { border-top: 2px solid var(--general-section-1); @@ -203,9 +75,27 @@ } } - &__offer-amt { - &__sell_ad { - margin-top: 0 !important; + &__wizard { + @include desktop { + width: 67.2rem; + } + + @include mobile { + padding: 1.6rem; + } + + .dc-form-progress { + background-color: var(--general-section-1); + margin-top: 2rem; + + &__step { + width: 24rem; + } + + &__steps { + margin-top: 0; + margin-bottom: 1.6rem; + } } } } diff --git a/packages/p2p/src/pages/my-ads/create-ad.jsx b/packages/p2p/src/pages/my-ads/create-ad.jsx index 793a81734718..38a26ec37828 100644 --- a/packages/p2p/src/pages/my-ads/create-ad.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad.jsx @@ -1,21 +1,14 @@ import * as React from 'react'; import { Loading } from '@deriv/components'; import { observer } from 'mobx-react-lite'; -import { localize } from 'Components/i18next'; -import PageReturn from 'Components/page-return'; import { useStores } from 'Stores'; import CreateAdForm from './create-ad-form.jsx'; const CreateAd = () => { const { my_ads_store } = useStores(); - const onClickBack = () => { - my_ads_store.setApiErrorMessage(''); - my_ads_store.setShowAdForm(false); - }; return ( - {my_ads_store.is_form_loading ? : } ); diff --git a/packages/p2p/src/pages/my-ads/edit-ad-form-payment-methods.jsx b/packages/p2p/src/pages/my-ads/edit-ad-form-payment-methods.jsx index 01039dd44247..e775bbd0eb0c 100755 --- a/packages/p2p/src/pages/my-ads/edit-ad-form-payment-methods.jsx +++ b/packages/p2p/src/pages/my-ads/edit-ad-form-payment-methods.jsx @@ -28,15 +28,6 @@ const EditAdFormPaymentMethods = ({ is_sell_advert, selected_methods, setSelecte touched(true); }; - React.useEffect(() => { - return () => { - my_ads_store.payment_method_ids = []; - my_ads_store.payment_method_names = []; - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - if (is_sell_advert) { if (p2p_advertiser_payment_methods?.length) { return ( diff --git a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx index 36c941c6f863..5f8588b05409 100644 --- a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx @@ -1,21 +1,18 @@ import * as React from 'react'; -import classNames from 'classnames'; -import { Formik, Field, Form } from 'formik'; -import { Button, Div100vhContainer, Input, Modal, Text, ThemedScrollbars } from '@deriv/components'; -import { formatMoney, isDesktop, isMobile } from '@deriv/shared'; +import { Formik, Form } from 'formik'; +import { Button, Div100vhContainer, FormProgress, Modal, Text, ThemedScrollbars, Wizard } from '@deriv/components'; +import { isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; -import { Localize, localize } from 'Components/i18next'; -import PageReturn from 'Components/page-return'; +import { localize } from 'Components/i18next'; import { api_error_codes } from 'Constants/api-error-codes'; import { buy_sell } from 'Constants/buy-sell'; import { useStores } from 'Stores'; import { ad_type } from 'Constants/floating-rate'; -import FloatingRate from 'Components/floating-rate'; import { generateErrorDialogTitle, generateErrorDialogBody } from 'Utils/adverts'; -import EditAdFormPaymentMethods from './edit-ad-form-payment-methods.jsx'; -import EditAdSummary from './edit-ad-summary.jsx'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; -import OrderTimeSelection from './order-time-selection'; +import AdConditionsSection from './ad-conditions-section'; +import AdPaymentDetailsSection from './ad-payment-details-section'; +import AdTypeSection from './ad-type-section'; import './edit-ad-form.scss'; const EditAdFormWrapper = ({ children }) => { @@ -28,13 +25,17 @@ const EditAdFormWrapper = ({ children }) => { const EditAdForm = () => { const { floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); + const [current_step, setCurrentStep] = React.useState(0); + const steps = [ + { header: { title: 'Edit ad type and amount' } }, + { header: { title: 'Edit payment details' } }, + { header: { title: 'Edit ad conditions' } }, + ]; const { - account_currency, amount_display, contact_info, description, - local_currency, max_order_amount_display, min_order_amount_display, order_expiry_period, @@ -48,7 +49,6 @@ const EditAdForm = () => { const is_buy_advert = type === buy_sell.BUY; const [selected_methods, setSelectedMethods] = React.useState([]); - const [is_payment_method_touched, setIsPaymentMethodTouched] = React.useState(false); const { useRegisterModalProps } = useModalManagerContext(); // when editing payment methods in creating an ad, once user declines to save their payment method, flow is to close all add payment method modals @@ -69,29 +69,6 @@ const EditAdForm = () => { return rate_display; }; - const payment_methods_changed = is_buy_advert - ? !( - !!payment_method_names && - selected_methods?.every(pm => { - const method = my_profile_store.getPaymentMethodDisplayName(pm); - return payment_method_names.includes(method); - }) && - selected_methods.length === payment_method_names.length - ) - : !( - !!payment_method_details && - selected_methods.every(pm => Object.keys(payment_method_details).includes(pm)) && - selected_methods.length === Object.keys(payment_method_details).length - ); - - const handleEditAdFormCancel = is_form_edited => { - if (is_form_edited || payment_methods_changed) { - general_store.showModal({ key: 'EditAdCancelModal', props: {} }); - } else { - my_ads_store.setShowEditAdForm(false); - } - }; - const is_api_error = [api_error_codes.ADVERT_SAME_LIMITS, api_error_codes.DUPLICATE_ADVERT].includes( my_ads_store.error_code ); @@ -121,25 +98,17 @@ const EditAdForm = () => { my_ads_store.payment_method_ids.push(pm[0]); }); } - if (my_ads_store.required_ad_type !== rate_type) { - const is_payment_method_available = - !!Object.keys({ ...payment_method_details }).length || - !!Object.values({ ...payment_method_names }).length; - setIsPaymentMethodTouched(is_payment_method_available); - } return () => { my_ads_store.setApiErrorCode(null); my_ads_store.setShowEditAdForm(false); + my_ads_store.payment_method_ids = []; + my_ads_store.payment_method_names = []; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( - my_ads_store.setShowEditAdForm(false)} - page_title={localize('Edit {{ad_type}} ad', { ad_type: type })} - /> { validate={my_ads_store.validateEditAdForm} validateOnMount > - {({ dirty, errors, handleChange, isSubmitting, isValid, setFieldTouched, touched, values }) => { - const is_sell_advert = values.type === buy_sell.SELL; - // Form should not be checked for value change when ad switch is triggered - const check_dirty = - my_ads_store.required_ad_type === rate_type - ? dirty || is_payment_method_touched - : is_payment_method_touched; + {() => { return (
-
-
- -
-
- - {({ field }) => ( - - {account_currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('offer_amount', true)} - hint={ - // Using two "==" is intentional as we're checking for nullish - // rather than falsy values. - !is_sell_advert || - general_store.advertiser_info.balance_available == null - ? undefined - : localize( - 'Your DP2P balance is {{ dp2p_balance }}', - { - dp2p_balance: `${formatMoney( - account_currency, - general_store.advertiser_info - .balance_available, - true - )} ${account_currency}`, - } - ) - } - is_relative_hint - disabled - /> - )} - - - {({ field }) => - my_ads_store.required_ad_type === ad_type.FLOAT ? ( - setFieldTouched('rate_type', true)} - required - change_handler={e => { - my_ads_store.restrictDecimalPlace(e, handleChange); - }} - place_holder='Floating rate' - {...field} - /> - ) : ( - - {local_currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('rate_type', true)} - required - /> - ) - } - -
-
- - {({ field }) => ( - - {account_currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('min_transaction', true)} - required - /> - )} - - - {({ field }) => ( - - {account_currency} - - } - onChange={e => { - my_ads_store.restrictLength(e, handleChange); - }} - onFocus={() => setFieldTouched('max_transaction', true)} - required - /> - )} - -
- {is_sell_advert && ( - - - {({ field }) => ( - - - - } - error={touched.contact_info && errors.contact_info} - className='edit-ad-form__field edit-ad-form__field--textarea' - initial_character_count={contact_info.length} - required - has_character_counter - max_characters={300} - onFocus={() => setFieldTouched('contact_info', true)} - /> - )} - - - )} - - {({ field }) => ( - - - - } - hint={localize('This information will be visible to everyone.')} - className='edit-ad-form__field edit-ad-form__field--textarea' - initial_character_count={description ? description.length : 0} - has_character_counter - max_characters={300} - onFocus={() => setFieldTouched('description', true)} - /> - )} - - - {({ field }) => } - -
- - - - - {is_sell_advert ? ( - - ) : ( - - )} - -
- -
-
- - -
+ setCurrentStep(step.active_step - 1)} + nav={} + > + + + +
diff --git a/packages/p2p/src/pages/my-ads/edit-ad-form.scss b/packages/p2p/src/pages/my-ads/edit-ad-form.scss index 76c29896790d..4fb3fad51a4b 100644 --- a/packages/p2p/src/pages/my-ads/edit-ad-form.scss +++ b/packages/p2p/src/pages/my-ads/edit-ad-form.scss @@ -11,9 +11,6 @@ } .dc-input__wrapper { - max-width: 67.2rem; // TODO: Kill these fixed widths. - margin-top: 2.1rem; - @include mobile { max-width: 90vw; diff --git a/packages/p2p/src/pages/my-ads/sell-ad-payment-methods-list.scss b/packages/p2p/src/pages/my-ads/sell-ad-payment-methods-list.scss index ca853fa98b3a..9f8152d46660 100644 --- a/packages/p2p/src/pages/my-ads/sell-ad-payment-methods-list.scss +++ b/packages/p2p/src/pages/my-ads/sell-ad-payment-methods-list.scss @@ -2,6 +2,7 @@ &__container { display: grid; grid-template-columns: repeat(4, 0.19fr); + column-gap: 2rem; &--horizontal { grid-auto-flow: column; diff --git a/packages/p2p/src/stores/my-ads-store.js b/packages/p2p/src/stores/my-ads-store.js index 7b2e446ed77b..f039c9354fcf 100644 --- a/packages/p2p/src/stores/my-ads-store.js +++ b/packages/p2p/src/stores/my-ads-store.js @@ -21,6 +21,8 @@ export default class MyAdsStore extends BaseStore { edit_ad_form_error = ''; error_message = ''; has_more_items_to_load = false; + min_join_days = 0; + min_completion_rate = 0; is_ad_created_modal_visible = false; is_edit_ad_error_modal_visible = false; is_form_loading = false; @@ -34,9 +36,9 @@ export default class MyAdsStore extends BaseStore { show_edit_ad_form = false; required_ad_type; error_code = ''; - payment_method_ids = []; payment_method_names = []; + preferred_countries = []; constructor(root_store) { // TODO: [mobx-undecorate] verify the constructor arguments and the arguments of this automatically generated super call @@ -99,7 +101,10 @@ export default class MyAdsStore extends BaseStore { setIsLoading: action.bound, setIsTableLoading: action.bound, setMaximumOrderAmount: action.bound, + setMinJoinDays: action.bound, + setMinCompletionRate: action.bound, setP2pAdvertInformation: action.bound, + setPreferredCountries: action.bound, setSelectedAdId: action.bound, setShouldShowAddPaymentMethod: action.bound, setShowAdForm: action.bound, @@ -190,6 +195,8 @@ export default class MyAdsStore extends BaseStore { order_expiry_period: values.order_completion_time, rate_type: this.root_store.floating_rate_store.rate_type, rate: Number(values.rate_type), + min_completion_rate: Number(this.min_completion_rate), + min_join_days: Number(this.min_join_days), ...(this.payment_method_names.length > 0 && !is_sell_ad ? { payment_method_names: this.payment_method_names } : {}), @@ -331,6 +338,8 @@ export default class MyAdsStore extends BaseStore { order_expiry_period: values.order_completion_time, rate_type: this.required_ad_type, rate: Number(values.rate_type), + min_completion_rate: Number(this.min_completion_rate), + min_join_days: Number(this.min_join_days), ...(this.payment_method_names.length > 0 && !is_sell_ad ? { payment_method_names: this.payment_method_names } : {}), @@ -532,10 +541,22 @@ export default class MyAdsStore extends BaseStore { this.maximum_order_amount = maximum_order_amount; } + setMinJoinDays(min_join_days) { + this.min_join_days = min_join_days; + } + + setMinCompletionRate(min_completion_rate) { + this.min_completion_rate = min_completion_rate; + } + setP2pAdvertInformation(p2p_advert_information) { this.p2p_advert_information = p2p_advert_information; } + setPreferredCountries(preferred_countries) { + this.preferred_countries = preferred_countries; + } + setSelectedAdId(selected_ad_id) { this.selected_ad_id = selected_ad_id; } From a7d6b159737e3a47198021eceb009c74f4f90862 Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Mon, 11 Mar 2024 17:18:24 +0400 Subject: [PATCH 04/17] fix: review comments --- .../__tests__/ad-cancel-modal.spec.tsx | 4 +- .../pages/advertiser-page/advertiser-page.jsx | 28 ++++++-- .../ad-conditions-section.tsx | 8 +-- .../ad-form-controller.scss | 9 ++- .../ad-progress-bar/ad-progress-bar.tsx | 53 +++++++++++++++ .../src/pages/my-ads/ad-progress-bar/index.ts | 3 + .../ad-type-section-trailing-icon.tsx | 17 +++++ .../ad-type-section/ad-type-section.scss | 1 + .../ad-type-section/ad-type-section.tsx | 42 ++---------- .../src/pages/my-ads/ad-wizard/ad-wizard.scss | 45 ++++++++++++ .../src/pages/my-ads/ad-wizard/ad-wizard.tsx | 68 +++++++++++++++++++ .../p2p/src/pages/my-ads/ad-wizard/index.ts | 4 ++ .../p2p/src/pages/my-ads/create-ad-form.jsx | 27 +------- .../p2p/src/pages/my-ads/create-ad-form.scss | 30 +++----- .../p2p/src/pages/my-ads/edit-ad-form.jsx | 18 +---- 15 files changed, 248 insertions(+), 109 deletions(-) create mode 100644 packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-progress-bar/index.ts create mode 100644 packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section-trailing-icon.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss create mode 100644 packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx create mode 100644 packages/p2p/src/pages/my-ads/ad-wizard/index.ts diff --git a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx index 71a644d0ce41..9f8fa5b02424 100644 --- a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/__tests__/ad-cancel-modal.spec.tsx @@ -44,7 +44,7 @@ describe('', () => { render(); - const dont_cancel_button = screen.getByText("Don't cancel"); + const dont_cancel_button = screen.getByRole('button', { name: "Don't cancel" }); userEvent.click(dont_cancel_button); expect(mock_modal_manager.hideModal).toHaveBeenCalled(); }); @@ -55,7 +55,7 @@ describe('', () => { render(); - const cancel_button = screen.getByText('Cancel'); + const cancel_button = screen.getByRole('button', { name: 'Cancel' }); userEvent.click(cancel_button); expect(onConfirm).toHaveBeenCalled(); }); diff --git a/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx b/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx index f55324862fcb..d6314a370c16 100755 --- a/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx +++ b/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx @@ -75,14 +75,30 @@ const AdvertiserPage = () => { const { data: p2p_advert_info } = useP2PAdvertInfo(counterparty_advert_id); - const showErrorModal = () => { + const showErrorModal = eligibility_status => { + let error_message = localize("It's either deleted or no longer active."); + let error_modal_title = localize('This ad is unavailable'); + + if (eligibility_status?.length > 0) { + error_modal_title = ''; + if (eligibility_status.length === 1) { + if (eligibility_status.includes('completion_rate')) { + error_message = localize('Your completion rate is too low for this ad.'); + } else if (eligibility_status.includes('join_date')) { + error_message = localize("You've not used Deriv P2P long enough for this ad."); + } + } else { + error_message = localize("The advertiser has set conditions for this ad that you don't meet."); + } + } + setCounterpartyAdvertId(''); showModal({ key: 'ErrorModal', props: { - error_message: "It's either deleted or no longer active.", + error_message, error_modal_button_text: 'OK', - error_modal_title: 'This ad is unavailable', + error_modal_title, onClose: () => { hideModal({ should_hide_all_modals: true }); }, @@ -94,16 +110,16 @@ const AdvertiserPage = () => { const setShowAdvertInfo = React.useCallback( () => { if (p2p_advert_info) { - const { is_active, is_buy, is_visible } = p2p_advert_info || {}; + const { eligibility_status, is_active, is_buy, is_eligible, is_visible } = p2p_advert_info || {}; const advert_type = is_buy ? 1 : 0; - if (is_active && is_visible) { + if (is_active && is_visible && is_eligible) { advertiser_page_store.setActiveIndex(advert_type); advertiser_page_store.handleTabItemClick(advert_type); buy_sell_store.setSelectedAdState(p2p_advert_info); showModal({ key: 'BuySellModal' }); } else { - showErrorModal(); + showErrorModal(eligibility_status); } } }, diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx index c9b91249bc51..7cacae7cb8e2 100644 --- a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormikValues, useFormikContext } from 'formik'; import { Button, Icon, Popover, Text } from '@deriv/components'; import BlockSelector from 'Components/block-selector'; -import { Localize } from 'Components/i18next'; +import { localize, Localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import AdFormController from 'Pages/my-ads/ad-form-controller'; import { useStores } from 'Stores'; @@ -44,14 +44,14 @@ const AdConditionsSection = ({ ...props }) => { { />
- + { + const radius = 28; + const circumference = 2 * Math.PI * radius; + const percentage = ((current_step + 1) / steps.length) * 100; + const offset = ((100 - percentage) * circumference) / 100; + + return ( + + + + + + + {current_step + 1} / {steps.length} + + + ); +}; + +export default AdProgressBar; diff --git a/packages/p2p/src/pages/my-ads/ad-progress-bar/index.ts b/packages/p2p/src/pages/my-ads/ad-progress-bar/index.ts new file mode 100644 index 000000000000..4fbd97819515 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-progress-bar/index.ts @@ -0,0 +1,3 @@ +import AdProgressBar from './ad-progress-bar'; + +export default AdProgressBar; diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section-trailing-icon.tsx b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section-trailing-icon.tsx new file mode 100644 index 000000000000..9b6ba27718ce --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section-trailing-icon.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Text } from '@deriv/components'; +import { useStore } from '@deriv/stores'; + +type TAdTypeSectionTrailingIcon = { label: string }; + +const AdTypeSectionTrailingIcon = ({ label }: TAdTypeSectionTrailingIcon) => { + const { ui: is_desktop } = useStore(); + + return ( + + {label} + + ); +}; + +export default AdTypeSectionTrailingIcon; diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss index 2631f0a49cf1..332dedb9831f 100644 --- a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss @@ -100,6 +100,7 @@ } @include mobile { + margin-top: 0; padding: 0.5rem 0; } } diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx index fef3f7433a34..d7cf9e3ba3e0 100644 --- a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx @@ -9,6 +9,7 @@ import { buy_sell } from 'Constants/buy-sell'; import { ad_type } from 'Constants/floating-rate'; import AdFormController from 'Pages/my-ads/ad-form-controller'; import { useStores } from 'Stores'; +import AdTypeSectionTrailingIcon from './ad-type-section-trailing-icon'; type AdTypeSection = { action?: string; @@ -17,7 +18,6 @@ type AdTypeSection = { const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { const { client: { currency, local_currency_config }, - ui: is_desktop, } = useStore(); const local_currency = local_currency_config.currency; const { buy_sell_store, floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); @@ -60,7 +60,7 @@ const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { general_store.showModal({ key: 'AdCancelModal', props: { - message: 'If you choose to cancel, the edited details will be lost.', + message: localize('If you choose to cancel, the edited details will be lost.'), onConfirm: () => { my_ads_store.setShowEditAdForm(false); }, @@ -74,7 +74,7 @@ const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { general_store.showModal({ key: 'AdCancelModal', props: { - message: "If you choose to cancel, the details you've entered will be lost.", + message: localize("If you choose to cancel, the details you've entered will be lost."), onConfirm: () => { my_ads_store.setApiErrorMessage(''); floating_rate_store.setApiErrorMessage(''); @@ -120,14 +120,7 @@ const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { error={touched.offer_amount && errors.offer_amount} label={localize('Total amount')} className='ad-type-section__field' - trailing_icon={ - - {currency} - - } + trailing_icon={} onFocus={() => setFieldTouched('offer_amount', true)} onChange={e => { my_ads_store.restrictLength(e, handleChange); @@ -179,14 +172,7 @@ const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { currency, })} className='ad-type-section__field' - trailing_icon={ - - {local_currency} - - } + trailing_icon={} onChange={e => { my_ads_store.restrictLength(e, handleChange); }} @@ -208,14 +194,7 @@ const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { error={touched.min_transaction && errors.min_transaction} label={localize('Min order')} className='ad-type-section__field' - trailing_icon={ - - {currency} - - } + trailing_icon={} onChange={e => { my_ads_store.restrictLength(e, handleChange); }} @@ -234,14 +213,7 @@ const AdTypeSection = ({ action = 'add', ...props }: AdTypeSection) => { error={touched.max_transaction && errors.max_transaction} label={localize('Max order')} className='ad-type-section__field' - trailing_icon={ - - {currency} - - } + trailing_icon={} onChange={e => { my_ads_store.restrictLength(e, handleChange); }} diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss new file mode 100644 index 000000000000..d441ac5c928c --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss @@ -0,0 +1,45 @@ +.ad-wizard { + @include desktop { + width: 67.2rem; + } + + > div:first-child { + @include mobile { + display: flex; + align-items: center; + column-gap: 1rem; + padding: 0.8rem 2rem; + background-color: var(--general-section-1); + + > div { + flex: 1; + } + + svg:last-child { + align-self: flex-start; + margin-top: 1.5rem; + width: 4rem; + } + } + } + + .wizard__main-step { + @include mobile { + padding: 1.6rem; + } + } + + .dc-form-progress { + background-color: var(--general-section-1); + margin-top: 2rem; + + &__step { + width: 24rem; + } + + &__steps { + margin-top: 0; + margin-bottom: 1.6rem; + } + } +} diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx new file mode 100644 index 000000000000..6b308afa09f4 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { DesktopWrapper, FormProgress, Icon, MobileWrapper, Text, Wizard } from '@deriv/components'; +import { Localize } from 'Components/i18next'; +import AdConditionsSection from 'Pages/my-ads/ad-conditions-section'; +import AdPaymentDetailsSection from 'Pages/my-ads/ad-payment-details-section'; +import AdProgressBar from 'Pages/my-ads/ad-progress-bar'; +import AdTypeSection from 'Pages/my-ads/ad-type-section'; + +type TStep = { header: { active_title: string; title: string }; sub_step_count: number }; +type TAdWizardNav = { + action: string; + steps: TStep[]; +}; + +const AdWizard = ({ action, steps }: TAdWizardNav) => { + const [current_step, setCurrentStep] = React.useState(0); + + return ( + setCurrentStep(step.active_step - 1)} + nav={ + <> + + + + +
+ +
+ + + + {steps[current_step + 1] ? ( + + + + ) : ( + + + + )} +
+ +
+
+ + } + > + + + +
+ ); +}; + +export default AdWizard; diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/index.ts b/packages/p2p/src/pages/my-ads/ad-wizard/index.ts new file mode 100644 index 000000000000..e0a961b6421c --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-wizard/index.ts @@ -0,0 +1,4 @@ +import AdWizard from './ad-wizard'; +import './ad-wizard.scss'; + +export default AdWizard; diff --git a/packages/p2p/src/pages/my-ads/create-ad-form.jsx b/packages/p2p/src/pages/my-ads/create-ad-form.jsx index 58c393e9608a..e7814bd51bbd 100644 --- a/packages/p2p/src/pages/my-ads/create-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad-form.jsx @@ -1,15 +1,6 @@ import * as React from 'react'; import { Formik, Form } from 'formik'; -import { - Button, - Checkbox, - Div100vhContainer, - FormProgress, - Modal, - Text, - ThemedScrollbars, - Wizard, -} from '@deriv/components'; +import { Button, Checkbox, Div100vhContainer, Modal, Text, ThemedScrollbars } from '@deriv/components'; import { isMobile } from '@deriv/shared'; import { observer } from '@deriv/stores'; import { reaction } from 'mobx'; @@ -19,9 +10,7 @@ import { ad_type } from 'Constants/floating-rate'; import { useStores } from 'Stores'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import { api_error_codes } from 'Constants/api-error-codes'; -import AdConditionsSection from './ad-conditions-section'; -import AdPaymentDetailsSection from './ad-payment-details-section'; -import AdTypeSection from './ad-type-section'; +import AdWizard from './ad-wizard'; import './create-ad-form.scss'; const CreateAdFormWrapper = ({ children }) => { @@ -34,7 +23,6 @@ const CreateAdFormWrapper = ({ children }) => { const CreateAdForm = () => { const { buy_sell_store, floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); const should_not_show_auto_archive_message_again = React.useRef(false); - const [current_step, setCurrentStep] = React.useState(0); const { useRegisterModalProps } = useModalManagerContext(); const steps = [ { header: { title: 'Set ad type and amount' } }, @@ -143,16 +131,7 @@ const CreateAdForm = () => { is_scrollbar_hidden={isMobile()} > - setCurrentStep(step.active_step - 1)} - nav={} - > - - - - + diff --git a/packages/p2p/src/pages/my-ads/create-ad-form.scss b/packages/p2p/src/pages/my-ads/create-ad-form.scss index 2d354e5d7d6a..fed6af863047 100644 --- a/packages/p2p/src/pages/my-ads/create-ad-form.scss +++ b/packages/p2p/src/pages/my-ads/create-ad-form.scss @@ -75,27 +75,13 @@ } } - &__wizard { - @include desktop { - width: 67.2rem; - } - - @include mobile { - padding: 1.6rem; - } - - .dc-form-progress { - background-color: var(--general-section-1); - margin-top: 2rem; - - &__step { - width: 24rem; - } - - &__steps { - margin-top: 0; - margin-bottom: 1.6rem; - } - } + &__progress-bar { + width: 5.6rem; + height: 5.6rem; + border-radius: 50%; + border: 2px solid; + display: flex; + align-items: center; + justify-content: center; } } diff --git a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx index 5f8588b05409..c676b776f2d9 100644 --- a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Formik, Form } from 'formik'; -import { Button, Div100vhContainer, FormProgress, Modal, Text, ThemedScrollbars, Wizard } from '@deriv/components'; +import { Button, Div100vhContainer, Modal, Text, ThemedScrollbars } from '@deriv/components'; import { isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; import { localize } from 'Components/i18next'; @@ -10,9 +10,7 @@ import { useStores } from 'Stores'; import { ad_type } from 'Constants/floating-rate'; import { generateErrorDialogTitle, generateErrorDialogBody } from 'Utils/adverts'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; -import AdConditionsSection from './ad-conditions-section'; -import AdPaymentDetailsSection from './ad-payment-details-section'; -import AdTypeSection from './ad-type-section'; +import AdWizard from './ad-wizard'; import './edit-ad-form.scss'; const EditAdFormWrapper = ({ children }) => { @@ -25,7 +23,6 @@ const EditAdFormWrapper = ({ children }) => { const EditAdForm = () => { const { floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); - const [current_step, setCurrentStep] = React.useState(0); const steps = [ { header: { title: 'Edit ad type and amount' } }, { header: { title: 'Edit payment details' } }, @@ -135,16 +132,7 @@ const EditAdForm = () => {
- setCurrentStep(step.active_step - 1)} - nav={} - > - - - - +
From 2236253e3c1a6221ee34873af98f0835fc2db40f Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Tue, 12 Mar 2024 12:11:54 +0400 Subject: [PATCH 05/17] fix: moved eligibility function to utils --- .../pages/advertiser-page/advertiser-page.jsx | 14 +++---------- .../pages/buy-sell/buy-sell-row-action.tsx | 21 +++++-------------- .../p2p/src/utils/__tests__/adverts.spec.ts | 21 ++++++++++++++++++- packages/p2p/src/utils/adverts.ts | 18 ++++++++++++++++ 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx b/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx index d6314a370c16..cfc99a1d4420 100755 --- a/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx +++ b/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx @@ -18,9 +18,9 @@ import TradeBadge from 'Components/trade-badge'; import UserAvatar from 'Components/user/user-avatar'; import { api_error_codes } from 'Constants/api-error-codes'; import { my_profile_tabs } from 'Constants/my-profile-tabs'; -import { getErrorMessage, getErrorModalTitle, getWidth } from 'Utils/block-user'; import { useStores } from 'Stores'; - +import { getEligibilityMessage } from 'Utils/adverts'; +import { getErrorMessage, getErrorModalTitle, getWidth } from 'Utils/block-user'; import AdvertiserPageAdverts from './advertiser-page-adverts.jsx'; import AdvertiserPageDropdownMenu from './advertiser-page-dropdown-menu.jsx'; import AdvertiserPageStats from './advertiser-page-stats.jsx'; @@ -81,15 +81,7 @@ const AdvertiserPage = () => { if (eligibility_status?.length > 0) { error_modal_title = ''; - if (eligibility_status.length === 1) { - if (eligibility_status.includes('completion_rate')) { - error_message = localize('Your completion rate is too low for this ad.'); - } else if (eligibility_status.includes('join_date')) { - error_message = localize("You've not used Deriv P2P long enough for this ad."); - } - } else { - error_message = localize("The advertiser has set conditions for this ad that you don't meet."); - } + error_message = getEligibilityMessage(eligibility_status); } setCounterpartyAdvertId(''); diff --git a/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx index 1bce2f370bda..2731f9ff8af6 100644 --- a/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx +++ b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { Button } from '@deriv/components'; import { useStore } from '@deriv/stores'; -import { Localize, localize } from 'Components/i18next'; +import { Localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import { useStores } from 'Stores'; +import { getEligibilityMessage } from 'Utils/adverts'; type TBuySellRowActionProps = { account_currency?: string; @@ -28,20 +29,8 @@ const BuySellRowAction = ({ } = useStore(); const { general_store } = useStores(); - const getEligibilityStatus = () => { - if (eligibility_status?.length === 1) { - if (eligibility_status.includes('completion_rate')) { - return localize('Your completion rate is too low for this ad.'); - } else if (eligibility_status.includes('join_date')) { - return localize("You've not used Deriv P2P long enough for this ad."); - } - } - - return localize("The advertiser has set conditions for this ad that you don't meet."); - }; - - const onUnavailableClick = () => { - showModal({ key: 'ErrorModal', props: { error_message: getEligibilityStatus() } }); + const onUnavailableClick = (eligibility_status: string[]) => { + showModal({ key: 'ErrorModal', props: { error_message: getEligibilityMessage(eligibility_status) } }); }; if (is_eligible) { @@ -77,7 +66,7 @@ const BuySellRowAction = ({ } return ( - ); diff --git a/packages/p2p/src/utils/__tests__/adverts.spec.ts b/packages/p2p/src/utils/__tests__/adverts.spec.ts index 13005daa91ef..301e599e509c 100644 --- a/packages/p2p/src/utils/__tests__/adverts.spec.ts +++ b/packages/p2p/src/utils/__tests__/adverts.spec.ts @@ -1,6 +1,11 @@ import moment from 'moment'; import { toMoment } from '@deriv/shared'; -import { generateErrorDialogBody, generateErrorDialogTitle, getLastOnlineLabel } from '../adverts'; +import { + generateErrorDialogBody, + generateErrorDialogTitle, + getEligibilityMessage, + getLastOnlineLabel, +} from '../adverts'; let mock_value: moment.Moment = moment(); jest.mock('@deriv/shared', () => ({ @@ -104,3 +109,17 @@ describe('getLastOnlineLabel', () => { expect(getLastOnlineLabel(is_online)).toBe('Seen more than 6 months ago'); }); }); + +describe('getEligibilityMessage', () => { + it('should return "Your completion rate is too low for this ad." if eligibility statuses only contains completion_rate', () => { + expect(getEligibilityMessage(['completion_rate'])).toBe('Your completion rate is too low for this ad.'); + }); + it('should return "You\'ve not used Deriv P2P long enough for this ad." if eligibility statuses only contains join_date', () => { + expect(getEligibilityMessage(['join_date'])).toBe("You've not used Deriv P2P long enough for this ad."); + }); + it('should return "The advertiser has set conditions for this ad that you don\'t meet." if eligibility statuses contains more than one reason', () => { + expect(getEligibilityMessage(['completion_rate, join_date'])).toBe( + "The advertiser has set conditions for this ad that you don't meet." + ); + }); +}); diff --git a/packages/p2p/src/utils/adverts.ts b/packages/p2p/src/utils/adverts.ts index 264be85f716e..e7ce500fc00a 100644 --- a/packages/p2p/src/utils/adverts.ts +++ b/packages/p2p/src/utils/adverts.ts @@ -114,3 +114,21 @@ export const getLastOnlineLabel = (is_online: 0 | 1, last_online_time?: number) } return localize('Online'); }; + +/** + * Function to get the message to be shown to users when they are not eligible to create an order against an advert. + * + * @param {string[]} eligibility_statuses - The list of reasons why the user is not eligible. + * @returns {string} The eligibility message based on the given eligibility statuses. + */ +export const getEligibilityMessage = (eligibility_statuses: string[]) => { + if (eligibility_statuses.length === 1) { + if (eligibility_statuses.includes('completion_rate')) { + return localize('Your completion rate is too low for this ad.'); + } else if (eligibility_statuses.includes('join_date')) { + return localize("You've not used Deriv P2P long enough for this ad."); + } + } + + return localize("The advertiser has set conditions for this ad that you don't meet."); +}; From c5816a2bb23c45046fc2a958fd68b33290d5dc64 Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Tue, 12 Mar 2024 13:05:08 +0400 Subject: [PATCH 06/17] fix: refactor code --- .../preferred-countries-modal.scss | 6 ++-- .../preferred-countries-modal.tsx | 4 +-- .../ad-form-controller/ad-form-controller.tsx | 1 - .../ad-payment-details-section.tsx | 25 +++++++++++++++- .../ad-type-section/ad-type-section.tsx | 30 +++++++------------ .../src/pages/my-ads/ad-wizard/ad-wizard.tsx | 4 ++- 6 files changed, 41 insertions(+), 29 deletions(-) diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss index 65bc41ed87af..61d40c9c7738 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss @@ -10,14 +10,12 @@ &__checkbox { padding: 1.6rem 1rem; } -} -.dc-modal__container_preferred-countries-modal { - .dc-modal-body { + &__body { padding: 0; } - .dc-modal-footer { + &__footer { > button { flex: 1; } diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx index 0db0dfdbbba9..d7c9a807c8ae 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx @@ -19,7 +19,7 @@ const PreferredCountriesModal = ({ onApply }: tPreferredCountriesModal) => { title={localize('Preferred countries')} toggleModal={() => hideModal()} > - + {({ submitForm, values: { search } }) => (
@@ -42,7 +42,7 @@ const PreferredCountriesModal = ({ onApply }: tPreferredCountriesModal) => { )} - + diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx index b2aac2d09c1b..ce1788e87f0e 100644 --- a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx @@ -3,7 +3,6 @@ import { Button } from '@deriv/components'; import { Localize } from 'Components/i18next'; type TAdFormControllerProps = { - action: string; getCurrentStep: () => number; getTotalSteps: () => number; goToFirstStep: () => void; diff --git a/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx index 2306f7968156..c934b5f97b52 100644 --- a/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx @@ -5,17 +5,40 @@ import { observer } from '@deriv/stores'; import { Localize } from 'Components/i18next'; import { buy_sell } from 'Constants/buy-sell'; import AdFormController from 'Pages/my-ads/ad-form-controller'; +import { useStores } from 'Stores'; import CreateAdFormPaymentMethods from '../create-ad-form-payment-methods.jsx'; import CreateAdSummary from '../create-ad-summary.jsx'; import OrderTimeSelection from '../order-time-selection'; -const AdPaymentDetailsSection = ({ ...props }) => { +type TAdPaymentDetailsSection = { + setIsFormDirty: React.Dispatch>; +}; + +const AdPaymentDetailsSection = ({ setIsFormDirty, ...props }: TAdPaymentDetailsSection) => { + const { my_ads_store, my_profile_store } = useStores(); const { errors, isValid, values } = useFormikContext(); const [selected_payment_methods, setSelectedPaymentMethods] = React.useState([]); const [is_next_btn_disabled, setIsNextBtnDisabled] = React.useState(true); const is_sell_advert = values.type === buy_sell.SELL; + const { payment_method_details, payment_method_names } = my_ads_store.p2p_advert_information; React.useEffect(() => { + const payment_methods_changed = is_sell_advert + ? !( + !!payment_method_details && + selected_payment_methods.every(pm => Object.keys(payment_method_details).includes(pm)) && + selected_payment_methods.length === Object.keys(payment_method_details).length + ) + : !( + !!payment_method_names && + selected_payment_methods?.every(pm => { + const method = my_profile_store.getPaymentMethodDisplayName(pm); + return payment_method_names.includes(method); + }) && + selected_payment_methods.length === payment_method_names.length + ); + + setIsFormDirty(payment_methods_changed); setIsNextBtnDisabled(!isValid || !selected_payment_methods?.length); }, [selected_payment_methods]); diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx index d9ec5ec568b2..1d8bf590cb33 100644 --- a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx @@ -14,20 +14,24 @@ import AdTypeSectionTrailingIcon from './ad-type-section-trailing-icon'; type AdTypeSection = { action?: string; float_rate_offset_limit_string: string; + is_form_dirty: boolean; rate_type: string; }; -const AdTypeSection = ({ action = 'add', float_rate_offset_limit_string, rate_type, ...props }: AdTypeSection) => { +const AdTypeSection = ({ + action = 'add', + float_rate_offset_limit_string, + is_form_dirty, + rate_type, + ...props +}: AdTypeSection) => { const { client: { currency, local_currency_config }, } = useStore(); const local_currency = local_currency_config.currency; - const { buy_sell_store, general_store, my_ads_store, my_profile_store } = useStores(); - const [selected_methods, setSelectedMethods] = React.useState([]); + const { buy_sell_store, general_store, my_ads_store } = useStores(); const { dirty, errors, handleChange, isValid, setFieldValue, setFieldTouched, touched, values } = useFormikContext(); - const { payment_method_details, payment_method_names, type } = my_ads_store.p2p_advert_information; - const is_buy_advert = type === buy_sell.BUY; const is_edit = action === 'edit'; const is_next_btn_disabled = is_edit ? !isValid : !dirty || !isValid; const onChangeAdTypeHandler = (user_input: string) => { @@ -44,21 +48,7 @@ const AdTypeSection = ({ action = 'add', float_rate_offset_limit_string, rate_ty const onCancel = () => { if (is_edit) { - const payment_methods_changed = is_buy_advert - ? !( - !!payment_method_names && - selected_methods?.every(pm => { - const method = my_profile_store.getPaymentMethodDisplayName(pm); - return payment_method_names.includes(method); - }) && - selected_methods.length === payment_method_names.length - ) - : !( - !!payment_method_details && - selected_methods.every(pm => Object.keys(payment_method_details).includes(pm)) && - selected_methods.length === Object.keys(payment_method_details).length - ); - if (dirty || payment_methods_changed) { + if (dirty || is_form_dirty) { general_store.showModal({ key: 'AdCancelModal', props: { diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx index 18159fd8e7d7..77161aabef98 100644 --- a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx @@ -16,6 +16,7 @@ type TAdWizardNav = { const AdWizard = ({ action, float_rate_offset_limit_string, rate_type, steps }: TAdWizardNav) => { const [current_step, setCurrentStep] = React.useState(0); + const [is_form_dirty, setIsFormDirty] = React.useState(false); return ( - + ); From 179afd3b8f4f621cde6ad289203d55d85ac39e7d Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Sun, 14 Apr 2024 22:15:06 +0400 Subject: [PATCH 07/17] feat: add eligible countries --- packages/api/types.ts | 6 + .../src/__tests__/useP2PCountryList.spec.tsx | 78 +++++++++++ packages/hooks/src/index.ts | 1 + packages/hooks/src/useP2PCountryList.ts | 22 +++ .../block-selector/block-selector.scss | 4 + .../block-selector/block-selector.tsx | 22 ++- .../modals/error-modal/error-modal.tsx | 2 +- .../preferred-countries-modal.spec.tsx | 42 ++++++ .../preferred-countries-modal-body.tsx | 120 +++++++++++++++++ .../preferred-countries-modal-footer.tsx | 24 ++++ .../preferred-countries-modal.scss | 44 +++++- .../preferred-countries-modal.tsx | 126 ++++++++++++------ .../ad-conditions-section.scss | 4 + .../ad-conditions-section.tsx | 51 +++---- .../ad-form-controller.scss | 5 +- .../ad-form-controller/ad-form-controller.tsx | 17 ++- .../ad-progress-bar/ad-progress-bar.tsx | 2 +- .../ad-type-section/ad-type-section.scss | 4 +- .../ad-type-section/ad-type-section.tsx | 1 + .../src/pages/my-ads/ad-wizard/ad-wizard.scss | 5 + .../src/pages/my-ads/ad-wizard/ad-wizard.tsx | 19 ++- .../p2p/src/pages/my-ads/create-ad-form.jsx | 9 +- .../p2p/src/pages/my-ads/edit-ad-form.jsx | 11 +- .../preferred-countries-selector.spec.tsx | 116 ++++++++++++++++ .../preferred-countries-selector/index.ts | 4 + .../preferred-countries-selector.scss | 9 ++ .../preferred-countries-selector.tsx | 63 +++++++++ packages/p2p/src/stores/my-ads-store.js | 6 +- packages/p2p/src/types/adverts.types.ts | 4 + 29 files changed, 724 insertions(+), 97 deletions(-) create mode 100644 packages/hooks/src/__tests__/useP2PCountryList.spec.tsx create mode 100644 packages/hooks/src/useP2PCountryList.ts create mode 100644 packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/__tests__/preferred-countries-modal.spec.tsx create mode 100644 packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx create mode 100644 packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-footer.tsx create mode 100644 packages/p2p/src/pages/my-ads/preferred-countries-selector/__tests__/preferred-countries-selector.spec.tsx create mode 100644 packages/p2p/src/pages/my-ads/preferred-countries-selector/index.ts create mode 100644 packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.scss create mode 100644 packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.tsx diff --git a/packages/api/types.ts b/packages/api/types.ts index 2d9ec5efea31..b82e87f02604 100644 --- a/packages/api/types.ts +++ b/packages/api/types.ts @@ -123,6 +123,8 @@ import type { P2PAdvertUpdateResponse, P2PChatCreateRequest, P2PChatCreateResponse, + P2PCountryListRequest, + P2PCountryListResponse, P2POrderCancelRequest, P2POrderCancelResponse, P2POrderConfirmRequest, @@ -2490,6 +2492,10 @@ type TSocketEndpoints = { request: P2PChatCreateRequest; response: P2PChatCreateResponse; }; + p2p_country_list: { + request: P2PCountryListRequest; + response: P2PCountryListResponse; + }; p2p_order_cancel: { request: P2POrderCancelRequest; response: P2POrderCancelResponse; diff --git a/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx b/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx new file mode 100644 index 000000000000..ea823f967394 --- /dev/null +++ b/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import { APIProvider, useQuery } from '@deriv/api'; +import { renderHook } from '@testing-library/react-hooks'; +import useP2PCountryList from '../useP2PCountryList'; + +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useQuery: jest.fn(), +})); + +const wrapper = ({ children }: { children: JSX.Element }) => {children}; +const mockUseQuery = useQuery as jest.MockedFunction>; + +describe('useP2PCountryList', () => { + it('should return undefined when there is no response', () => { + // @ts-expect-error need to come up with a way to mock the return type of useQuery + mockUseQuery.mockReturnValue({ data: {} }); + const { result } = renderHook(() => useP2PCountryList(), { wrapper }); + expect(result.current.data).toBeUndefined(); + }); + + it('should return country list with the correct details', () => { + const mockQueryData = { + p2p_country_list: { + ai: { + country_name: 'Anguilla', + cross_border_ads_enabled: 1, + fixed_rate_adverts: 'enabled', + float_rate_adverts: 'disabled', + float_rate_offset_limit: 10, + local_currency: 'XCD', + payment_methods: { + alipay: { + display_name: 'Alipay', + fields: { + account: { + display_name: 'Alipay ID', + required: 1, + type: 'text', + }, + instructions: { + display_name: 'Instructions', + required: 0, + type: 'memo', + }, + }, + type: 'ewallet', + }, + }, + }, + }, + }; + // @ts-expect-error need to come up with a way to mock the return type of useQuery + mockUseQuery.mockReturnValue({ + data: mockQueryData, + }); + + const { result } = renderHook(() => useP2PCountryList(), { wrapper }); + const p2p_country_list = result.current.data; + if (p2p_country_list) { + expect(p2p_country_list).toEqual(mockQueryData.p2p_country_list); + } + }); + it('should call the useQuery with parameters if passed', () => { + renderHook(() => useP2PCountryList({ country: 'id' }), { wrapper }); + expect(mockUseQuery).toHaveBeenCalledWith('p2p_country_list', { + payload: { country: 'id' }, + options: { refetchOnWindowFocus: false }, + }); + }); + it('should call the useQuery with default parameters if not passed', () => { + renderHook(() => useP2PCountryList(), { wrapper }); + expect(mockUseQuery).toHaveBeenCalledWith('p2p_country_list', { + payload: undefined, + options: { refetchOnWindowFocus: false }, + }); + }); +}); diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index f5d0ff3605a5..28513474b641 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -56,6 +56,7 @@ export { default as useP2PAdvertInfo } from './useP2PAdvertInfo'; export { default as useP2PAdvertList } from './useP2PAdvertList'; export { default as useP2PAdvertiserPaymentMethods } from './useP2PAdvertiserPaymentMethods'; export { default as useP2PCompletedOrdersNotification } from './useP2PCompletedOrdersNotification'; +export { default as useP2PCountryList } from './useP2PCountryList'; export { default as useP2PExchangeRate } from './useP2PExchangeRate'; export { default as useP2PNotificationCount } from './useP2PNotificationCount'; export { default as useP2POrderList } from './useP2POrderList'; diff --git a/packages/hooks/src/useP2PCountryList.ts b/packages/hooks/src/useP2PCountryList.ts new file mode 100644 index 000000000000..d5b0738331e0 --- /dev/null +++ b/packages/hooks/src/useP2PCountryList.ts @@ -0,0 +1,22 @@ +import { useQuery } from '@deriv/api'; + +/** + * A custom hook that returns an object containing the list of countries available for P2P trading. + * + * For returning details of a specific country, the country code can be passed in the payload. + * @example: useCountryList({ country: 'id' }) + * + */ +const useP2PCountryList = (payload?: NonNullable>[1]>['payload']) => { + const { data, ...rest } = useQuery('p2p_country_list', { + payload, + options: { refetchOnWindowFocus: false }, + }); + + return { + p2p_country_list: data?.p2p_country_list, + ...rest, + }; +}; + +export default useP2PCountryList; diff --git a/packages/p2p/src/components/block-selector/block-selector.scss b/packages/p2p/src/components/block-selector/block-selector.scss index 043d3a0f57be..95c92ff605ac 100644 --- a/packages/p2p/src/components/block-selector/block-selector.scss +++ b/packages/p2p/src/components/block-selector/block-selector.scss @@ -10,6 +10,10 @@ &__options { display: flex; column-gap: 1rem; + + @include mobile { + justify-content: space-between; + } } &__option { diff --git a/packages/p2p/src/components/block-selector/block-selector.tsx b/packages/p2p/src/components/block-selector/block-selector.tsx index 04ccdfa7fd18..6c2115be3801 100644 --- a/packages/p2p/src/components/block-selector/block-selector.tsx +++ b/packages/p2p/src/components/block-selector/block-selector.tsx @@ -1,7 +1,8 @@ import React from 'react'; import classNames from 'classnames'; -import { Icon, Popover, Text } from '@deriv/components'; -import { Localize } from 'Components/i18next'; +import { Icon, Text } from '@deriv/components'; +import { localize, Localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; type TBlockSelectorOptionProps = { is_selected?: boolean; @@ -19,6 +20,7 @@ type TBlockSelectorProps = { const BlockSelector = ({ label, onSelect, options, tooltip_info, value }: TBlockSelectorProps) => { const [selectors, setSelectors] = React.useState(options); const [selected_value, setSelectedValue] = React.useState(value); + const { showModal } = useModalManagerContext(); const onClick = e => { const selected_value = e.target.getAttribute('data-value'); onSelect?.(0); @@ -44,9 +46,19 @@ const BlockSelector = ({ label, onSelect, options, tooltip_info, value }: TBlock - }> - - + { + showModal({ + key: 'ErrorModal', + props: { + error_message: localize(tooltip_info), + error_modal_title: localize(label), + }, + }); + }} + />
{selectors.map(option => ( diff --git a/packages/p2p/src/components/modal-manager/modals/error-modal/error-modal.tsx b/packages/p2p/src/components/modal-manager/modals/error-modal/error-modal.tsx index 8f6a3f2070e7..6a5d2dec71a4 100644 --- a/packages/p2p/src/components/modal-manager/modals/error-modal/error-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/error-modal/error-modal.tsx @@ -33,7 +33,7 @@ const ErrorModal = ({ width={is_mobile ? '90rem' : '40rem'} > - + diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/__tests__/preferred-countries-modal.spec.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/__tests__/preferred-countries-modal.spec.tsx new file mode 100644 index 000000000000..2ba20ebf74ee --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/__tests__/preferred-countries-modal.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import PreferredCountriesModal from '../preferred-countries-modal'; + +const country_list = [ + { text: 'ad', value: 'Andorra' }, + { text: 'af', value: 'Afghanistan' }, +]; + +const mock_modal_manager: Partial> = { + hideModal: jest.fn(), + is_modal_open: true, +}; + +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn(() => mock_modal_manager), +})); + +const el_modal = document.createElement('div'); +const wrapper = ({ children }) => {children}; + +describe('', () => { + beforeAll(() => { + el_modal.setAttribute('id', 'modal_root'); + document.body.appendChild(el_modal); + }); + afterAll(() => { + document.body.removeChild(el_modal); + }); + it('should render the component', () => { + render(, { wrapper }); + expect(screen.getByText('Preferred countries')).toBeInTheDocument(); + + const checkbox_ad = screen.getByRole('checkbox', { name: 'ad' }); + const checkbox_af = screen.getByRole('checkbox', { name: 'af' }); + expect(checkbox_ad).toBeInTheDocument(); + expect(checkbox_af).toBeInTheDocument(); + }); +}); diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx new file mode 100644 index 000000000000..64b02fd8e224 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Checkbox, Icon, Input, Text, ThemedScrollbars } from '@deriv/components'; +import { useStore } from '@deriv/stores'; +import { localize, Localize } from 'Components/i18next'; + +type TPreferredCountriesModalBodyProps = { + country_list: { text: string; value: string }[]; + eligible_countries: string[]; + search_value: string; + setSearchValue: (value: string) => void; + selected_countries: string[]; + setSelectedCountries: (value: string[]) => void; +}; + +const PreferredCountriesModalBody = ({ + country_list, + eligible_countries, + search_value, + setSearchValue, + selected_countries, + setSelectedCountries, +}: TPreferredCountriesModalBodyProps) => { + const { + ui: { is_desktop }, + } = useStore(); + const [search_results, setSearchResults] = React.useState([ + ...country_list.filter(item => eligible_countries.includes(item.value)), + ...country_list.filter(item => !eligible_countries.includes(item.value)), + ]); + + const onClearSearch = () => { + setSearchValue(''); + setSearchResults([ + ...country_list.filter(item => selected_countries.includes(item.value)), + ...country_list.filter(item => !selected_countries.includes(item.value)), + ]); + }; + + const onSearch = e => { + const { value } = e.target; + if (!value) { + onClearSearch(); + return; + } + + setSearchValue(value); + setSearchResults(country_list.filter(item => item.text.toLowerCase().includes(value.toLowerCase()))); + }; + + return ( + <> + } + onChange={onSearch} + placeholder={localize('Search countries')} + trailing_icon={ + search_value ? : null + } + type='text' + value={search_value} + /> + + {search_results?.length > 0 ? ( + <> + 0 && + selected_countries?.length !== country_list?.length, + })} + value={selected_countries?.length === country_list?.length} + label='All countries' + name='all' + onChange={(event: React.ChangeEvent) => { + if (event.target.checked) { + setSelectedCountries(country_list.map(item => item.value)); + } else { + setSelectedCountries([]); + } + }} + /> + {search_results?.map(item => ( + ) => { + if (event.target.checked) { + setSelectedCountries([...selected_countries, item.value]); + } else { + setSelectedCountries(selected_countries.filter(value => value !== item.value)); + } + }} + /> + ))} + + ) : ( +
+ + + + + + +
+ )} +
+ + ); +}; + +export default PreferredCountriesModalBody; diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-footer.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-footer.tsx new file mode 100644 index 000000000000..c6696915abf7 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-footer.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Button } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +type TPreferredCountriesModalFooterProps = { + is_disabled: boolean; + onClear: () => void; + onApply: () => void; +}; + +const PreferredCountriesModalFooter = ({ is_disabled, onClear, onApply }: TPreferredCountriesModalFooterProps) => { + return ( + <> + + + + ); +}; + +export default PreferredCountriesModalFooter; diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss index 61d40c9c7738..e41662b248f8 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss @@ -2,20 +2,62 @@ &__search-field { margin-bottom: 0; + &--icon { + margin-left: 1.8rem !important; + } + input { - margin-left: 2.5rem; + margin-left: 3.3rem; + } + + .dc-input__container { + border-radius: 0; } } &__checkbox { padding: 1.6rem 1rem; + + &--inactive { + .dc-checkbox__box { + background-color: var(--brand-red-coral); + border-color: var(--brand-red-coral); + } + + input[name='all'] + span:after { + content: ''; + border: 1px solid var(--brand-white); + border-radius: 0.1rem; + width: 0.8rem; + margin: auto; + } + } + } + + &__no-results { + height: 48rem; + display: flex; + flex-direction: column; + justify-content: center; } &__body { padding: 0; + + @include mobile { + flex-direction: column; + } } &__footer { + @include desktop { + padding-right: 2.4rem; + } + + @include mobile { + column-gap: 0.8rem; + } + > button { flex: 1; } diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx index d7c9a807c8ae..0efa62dd9de0 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx @@ -1,56 +1,96 @@ import React from 'react'; -import { Field, Form, Formik, FormikValues } from 'formik'; -import { Button, Icon, Input, Modal } from '@deriv/components'; +import { DesktopWrapper, MobileFullPageModal, MobileWrapper, Modal, Text } from '@deriv/components'; import { localize, Localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import PreferredCountriesModalBody from './preferred-countries-modal-body'; +import PreferredCountriesModalFooter from './preferred-countries-modal-footer'; -type tPreferredCountriesModal = { - onApply?: () => void; +type TPreferredCountriesModal = { + country_list: { text: string; value: string }[]; + eligible_countries: string[]; + onApply?: (value: string[]) => void; }; -const PreferredCountriesModal = ({ onApply }: tPreferredCountriesModal) => { +const PreferredCountriesModal = ({ country_list, eligible_countries, onApply }: TPreferredCountriesModal) => { + const [search_value, setSearchValue] = React.useState(''); + const [selected_countries, setSelectedCountries] = React.useState(eligible_countries); const { hideModal, is_modal_open } = useModalManagerContext(); + const onApplySelectedCountries = () => { + onApply?.(selected_countries); + hideModal(); + }; + return ( - hideModal()} - > - - - {({ submitForm, values: { search } }) => ( - - - {({ field }: FormikValues) => ( - } - name='search' - onFocus={submitForm} - placeholder={localize('Search countries')} - trailing_icon={search ? : null} - type='text' - /> - )} - - + + + hideModal()} + > + + + + {!search_value && ( + + { + setSelectedCountries(eligible_countries); + }} + onApply={onApplySelectedCountries} + /> + + )} + + + + + + + } + renderPageFooterChildren={() => ( + { + setSelectedCountries(eligible_countries); + }} + onApply={onApplySelectedCountries} + /> )} - - - - - - - + > + + + + ); }; export default PreferredCountriesModal; diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss index 03b10159e711..627f89597254 100644 --- a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss @@ -4,4 +4,8 @@ align-items: center; column-gap: 1rem; } + + &__label { + margin-top: 2rem; + } } diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx index 7cacae7cb8e2..8b0f9021ca04 100644 --- a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx @@ -1,16 +1,22 @@ import React from 'react'; import { FormikValues, useFormikContext } from 'formik'; -import { Button, Icon, Popover, Text } from '@deriv/components'; +import { Icon, Text } from '@deriv/components'; import BlockSelector from 'Components/block-selector'; import { localize, Localize } from 'Components/i18next'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import AdFormController from 'Pages/my-ads/ad-form-controller'; +import PreferredCountriesSelector from 'Pages/my-ads/preferred-countries-selector'; import { useStores } from 'Stores'; +import { TCountryListProps } from 'Types'; import CreateAdSummary from '../create-ad-summary.jsx'; -const AdConditionsSection = ({ ...props }) => { - const { errors, isSubmitting, isValid, values } = useFormikContext(); +type TAdConditionsSection = { + action: string; + country_list: TCountryListProps; +}; +const AdConditionsSection = ({ action, country_list, ...props }: TAdConditionsSection) => { const { showModal } = useModalManagerContext(); + const { errors, isSubmitting, isValid, values } = useFormikContext(); const { my_ads_store } = useStores(); const { min_completion_rate, min_join_days } = my_ads_store.p2p_advert_information; const joining_days = [ @@ -37,7 +43,7 @@ const AdConditionsSection = ({ ...props }) => { price_rate={values.rate_type} type={values.type} /> - + @@ -61,27 +67,24 @@ const AdConditionsSection = ({ ...props }) => { - - } - > - - + { + showModal({ + key: 'ErrorModal', + props: { + error_message: localize( + 'We’ll only show your ad to people in the countries you choose.' + ), + error_modal_title: localize('Preferred countries'), + }, + }); + }} + />
- - + + ); }; diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss index 6fdc49b9ef19..27d97fb231c5 100644 --- a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss @@ -6,10 +6,11 @@ padding: 2rem 0; @include desktop { - margin-top: 10rem; + margin-top: 5rem; } @include mobile { - margin-top: 3rem; + margin: 3rem -1.6rem 0; + padding-right: 1.6rem; } } diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx index ce1788e87f0e..ecfef4b3446a 100644 --- a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx @@ -3,31 +3,30 @@ import { Button } from '@deriv/components'; import { Localize } from 'Components/i18next'; type TAdFormControllerProps = { + action?: string; getCurrentStep: () => number; getTotalSteps: () => number; - goToFirstStep: () => void; - goToStep: () => void; - goToLastStep: () => void; goToNextStep: () => void; goToPreviousStep: () => void; is_next_btn_disabled: boolean; - onCancel: () => void; + onCancel?: () => void; }; const AdFormController = ({ + action, getCurrentStep, getTotalSteps, - goToFirstStep, - goToStep, - goToLastStep, goToNextStep, goToPreviousStep, is_next_btn_disabled, onCancel, }: TAdFormControllerProps) => { + const post_btn_text = + action === 'edit' ? : ; + return (
- {getCurrentStep() === 1 && ( + {getCurrentStep() === 1 && onCancel && ( @@ -58,7 +57,7 @@ const AdFormController = ({ ) : ( )}
diff --git a/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx b/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx index 331a52e84f57..7647fd97c547 100644 --- a/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx +++ b/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx @@ -1,6 +1,6 @@ import React from 'react'; -type TStep = { header: { active_title: string; title: string }; sub_step_count: number }; +type TStep = { header: { title: string }; sub_step_count: number }; type TAdProgressBar = { current_step: number; steps: TStep[]; diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss index 332dedb9831f..00ed1e05c973 100644 --- a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss @@ -55,12 +55,12 @@ } &--textarea { height: 9.6rem; - margin-bottom: 0; + margin: 2rem 0 0; width: 67.2rem; @include mobile { height: 9.8rem; - width: 90vw; + width: auto; } .dc-input { diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx index 1d8bf590cb33..737bc2eace92 100644 --- a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx @@ -128,6 +128,7 @@ const AdTypeSection = ({ }) } is_relative_hint + disabled={is_edit} /> )} diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss index d441ac5c928c..a6342a2f3765 100644 --- a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss @@ -32,6 +32,7 @@ .dc-form-progress { background-color: var(--general-section-1); margin-top: 2rem; + border-radius: 0.4rem; &__step { width: 24rem; @@ -41,5 +42,9 @@ margin-top: 0; margin-bottom: 1.6rem; } + + &__header { + margin-bottom: 2rem; + } } } diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx index 77161aabef98..a132f880330a 100644 --- a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx @@ -5,16 +5,27 @@ import AdConditionsSection from 'Pages/my-ads/ad-conditions-section'; import AdPaymentDetailsSection from 'Pages/my-ads/ad-payment-details-section'; import AdProgressBar from 'Pages/my-ads/ad-progress-bar'; import AdTypeSection from 'Pages/my-ads/ad-type-section'; +import { TCountryListProps } from 'Types'; -type TStep = { header: { active_title: string; title: string }; sub_step_count: number }; +type TStep = { header: { title: string }; sub_step_count: number }; type TAdWizardNav = { action: string; + country_list: TCountryListProps; + default_step?: number; float_rate_offset_limit_string: string; + onClose: () => void; rate_type: string; steps: TStep[]; }; -const AdWizard = ({ action, float_rate_offset_limit_string, rate_type, steps }: TAdWizardNav) => { +const AdWizard = ({ + action, + country_list, + float_rate_offset_limit_string, + onClose, + rate_type, + steps, +}: TAdWizardNav) => { const [current_step, setCurrentStep] = React.useState(0); const [is_form_dirty, setIsFormDirty] = React.useState(false); @@ -55,7 +66,7 @@ const AdWizard = ({ action, float_rate_offset_limit_string, rate_type, steps }: )}
- +
@@ -68,7 +79,7 @@ const AdWizard = ({ action, float_rate_offset_limit_string, rate_type, steps }: rate_type={rate_type} /> - + ); }; diff --git a/packages/p2p/src/pages/my-ads/create-ad-form.jsx b/packages/p2p/src/pages/my-ads/create-ad-form.jsx index 2bbb78399e11..5adfb395ecf1 100644 --- a/packages/p2p/src/pages/my-ads/create-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad-form.jsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Formik, Form } from 'formik'; import { Button, Checkbox, Div100vhContainer, Modal, Text, ThemedScrollbars } from '@deriv/components'; -import { useP2PSettings } from '@deriv/hooks'; +import { useP2PCountryList, useP2PSettings } from '@deriv/hooks'; import { isMobile } from '@deriv/shared'; import { observer } from '@deriv/stores'; import { reaction } from 'mobx'; @@ -31,6 +31,7 @@ const CreateAdForm = () => { rate_type, }, } = useP2PSettings(); + const { p2p_country_list = {} } = useP2PCountryList(); const should_not_show_auto_archive_message_again = React.useRef(false); const { useRegisterModalProps } = useModalManagerContext(); @@ -103,9 +104,11 @@ const CreateAdForm = () => { return ( { > { + my_ads_store.setShowAdForm(false); + }} rate_type={rate_type} steps={steps} /> diff --git a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx index c582ed8a2df2..8e353b1ab54e 100644 --- a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Formik, Form } from 'formik'; import { Button, Div100vhContainer, Modal, Text, ThemedScrollbars } from '@deriv/components'; -import { useP2PSettings } from '@deriv/hooks'; +import { useP2PCountryList, useP2PSettings } from '@deriv/hooks'; import { isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; import { localize } from 'Components/i18next'; @@ -34,6 +34,7 @@ const EditAdForm = () => { amount_display, contact_info, description, + eligible_countries, max_order_amount_display, min_order_amount_display, order_expiry_period, @@ -48,6 +49,7 @@ const EditAdForm = () => { const is_buy_advert = type === buy_sell.BUY; const [selected_methods, setSelectedMethods] = React.useState([]); const { useRegisterModalProps } = useModalManagerContext(); + const { p2p_country_list } = useP2PCountryList(); const { p2p_settings } = useP2PSettings(); // when editing payment methods in creating an ad, once user declines to save their payment method, flow is to close all add payment method modals @@ -108,7 +110,8 @@ const EditAdForm = () => { { { + my_ads_store.setShowEditAdForm(false); + }} rate_type={p2p_settings.rate_type} steps={steps} /> diff --git a/packages/p2p/src/pages/my-ads/preferred-countries-selector/__tests__/preferred-countries-selector.spec.tsx b/packages/p2p/src/pages/my-ads/preferred-countries-selector/__tests__/preferred-countries-selector.spec.tsx new file mode 100644 index 000000000000..73d9fbd25c6e --- /dev/null +++ b/packages/p2p/src/pages/my-ads/preferred-countries-selector/__tests__/preferred-countries-selector.spec.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { render, screen } from '@testing-library/react'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import PreferredCountriesSelector from '../preferred-countries-selector'; + +const mock_modal_manager: Partial> = { + showModal: jest.fn(), +}; +jest.mock('Components/modal-manager/modal-manager-context', () => ({ + ...jest.requireActual('Components/modal-manager/modal-manager-context'), + useModalManagerContext: jest.fn(() => mock_modal_manager), +})); +const country_list = { + ad: { + country_name: 'Andorra', + cross_border_ads_enabled: 1, + fixed_rate_adverts: 'enabled', + float_rate_adverts: 'disabled', + float_rate_offset_limit: 10, + local_currency: 'EUR', + payment_methods: { + alipay: { + display_name: 'Alipay', + fields: { + account: { + display_name: 'Alipay ID', + required: 1, + type: 'text', + }, + instructions: { + display_name: 'Instructions', + required: 0, + type: 'memo', + }, + }, + type: 'ewallet', + }, + }, + }, + af: { + country_name: 'Afghanistan', + cross_border_ads_enabled: 1, + fixed_rate_adverts: 'enabled', + float_rate_adverts: 'disabled', + float_rate_offset_limit: 10, + local_currency: 'AFN', + payment_methods: { + alipay: { + display_name: 'Alipay', + fields: { + account: { + display_name: 'Alipay ID', + required: 1, + type: 'text', + }, + instructions: { + display_name: 'Instructions', + required: 0, + type: 'memo', + }, + }, + type: 'ewallet', + }, + }, + }, + ai: { + country_name: 'Anguilla', + cross_border_ads_enabled: 1, + fixed_rate_adverts: 'enabled', + float_rate_adverts: 'disabled', + float_rate_offset_limit: 10, + local_currency: 'XCD', + payment_methods: { + alipay: { + display_name: 'Alipay', + fields: { + account: { + display_name: 'Alipay ID', + required: 1, + type: 'text', + }, + instructions: { + display_name: 'Instructions', + required: 0, + type: 'memo', + }, + }, + type: 'ewallet', + }, + }, + }, +}; + +describe('', () => { + it('should render the component', () => { + render( + + + + ); + + const selector = screen.getByRole('textbox'); + expect(selector).toHaveValue('All countries'); + }); + it('should render the eligible countries', () => { + render( + + + + ); + + const selector = screen.getByRole('textbox'); + expect(selector).toHaveValue('Andorra, Afghanistan'); + }); +}); diff --git a/packages/p2p/src/pages/my-ads/preferred-countries-selector/index.ts b/packages/p2p/src/pages/my-ads/preferred-countries-selector/index.ts new file mode 100644 index 000000000000..904cdbcb06a7 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/preferred-countries-selector/index.ts @@ -0,0 +1,4 @@ +import PreferredCountriesSelector from './preferred-countries-selector'; +import './preferred-countries-selector.scss'; + +export default PreferredCountriesSelector; diff --git a/packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.scss b/packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.scss new file mode 100644 index 000000000000..887527ae8fec --- /dev/null +++ b/packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.scss @@ -0,0 +1,9 @@ +.preferred-countries-selector { + @include desktop { + max-width: 32.8rem !important; + } + + &__input { + text-overflow: ellipsis; + } +} diff --git a/packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.tsx b/packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.tsx new file mode 100644 index 000000000000..8d1f60b92c4c --- /dev/null +++ b/packages/p2p/src/pages/my-ads/preferred-countries-selector/preferred-countries-selector.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Field, FormikHelpers, FormikValues, useFormikContext } from 'formik'; +import { Icon, Input } from '@deriv/components'; +import { localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; +import { TCountryListProps } from 'Types'; + +type TFormikContext = { + setFieldValue?: FormikHelpers['setFieldValue']; + values: FormikValues; +}; +type TPreferredCountriesSelectorProps = { + country_list: TCountryListProps; +}; + +const PreferredCountriesSelector = ({ country_list }: TPreferredCountriesSelectorProps) => { + const { setFieldValue, values }: TFormikContext = useFormikContext(); + const { showModal } = useModalManagerContext(); + const countries = Object.keys(country_list).map(key => ({ + text: country_list[key]?.country_name, + value: key, + })); + + const getSelectedCountriesText = () => { + const eligible_countries = values.eligible_countries; + if (eligible_countries?.length === countries.length) { + return localize('All countries'); + } + + return eligible_countries?.map((value: string) => country_list[value]?.country_name).join(', '); + }; + + return ( + + {({ field }: FormikValues) => ( + { + showModal({ + key: 'PreferredCountriesModal', + props: { + country_list: countries, + eligible_countries: values.eligible_countries, + onApply: value => { + setFieldValue('eligible_countries', [...value]); + getSelectedCountriesText(); + }, + }, + }); + }} + readOnly + trailing_icon={} + type='text' + value={getSelectedCountriesText()} + /> + )} + + ); +}; + +export default PreferredCountriesSelector; diff --git a/packages/p2p/src/stores/my-ads-store.js b/packages/p2p/src/stores/my-ads-store.js index af33f61b56a4..ac81704c39ed 100644 --- a/packages/p2p/src/stores/my-ads-store.js +++ b/packages/p2p/src/stores/my-ads-store.js @@ -169,6 +169,7 @@ export default class MyAdsStore extends BaseStore { const create_advert = { p2p_advert_create: 1, type: values.type, + eligible_countries: values.eligible_countries, amount: Number(values.offer_amount), max_order_amount: Number(values.max_transaction), min_order_amount: Number(values.min_transaction), @@ -305,6 +306,7 @@ export default class MyAdsStore extends BaseStore { const update_advert = { p2p_advert_update: 1, id: this.selected_ad_id, + eligible_countries: values.eligible_countries, max_order_amount: Number(values.max_transaction), min_order_amount: Number(values.min_transaction), order_expiry_period: values.order_completion_time, @@ -324,8 +326,8 @@ export default class MyAdsStore extends BaseStore { update_advert.contact_info = values.contact_info; } - if (values.description) { - update_advert.description = values.description; + if (values.default_advert_description) { + update_advert.description = values.default_advert_description; } if (values.reached_target_date) { update_advert.is_active = values.is_active; diff --git a/packages/p2p/src/types/adverts.types.ts b/packages/p2p/src/types/adverts.types.ts index f87d045e709b..6c3564776388 100644 --- a/packages/p2p/src/types/adverts.types.ts +++ b/packages/p2p/src/types/adverts.types.ts @@ -56,3 +56,7 @@ export type TAdvertProps = { type: string; visibility_status: string[]; }; + +export type TCountryListProps = { + [key: string]: { country_name: string }; +}; From af6d199ce54acb51e515317d8376233a3bfd1932 Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Mon, 15 Apr 2024 12:27:09 +0400 Subject: [PATCH 08/17] fix: test --- packages/hooks/src/__tests__/useP2PCountryList.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx b/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx index ea823f967394..5db65047eda1 100644 --- a/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx +++ b/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx @@ -16,7 +16,7 @@ describe('useP2PCountryList', () => { // @ts-expect-error need to come up with a way to mock the return type of useQuery mockUseQuery.mockReturnValue({ data: {} }); const { result } = renderHook(() => useP2PCountryList(), { wrapper }); - expect(result.current.data).toBeUndefined(); + expect(result.current.p2p_country_list).toBeUndefined(); }); it('should return country list with the correct details', () => { @@ -56,7 +56,7 @@ describe('useP2PCountryList', () => { }); const { result } = renderHook(() => useP2PCountryList(), { wrapper }); - const p2p_country_list = result.current.data; + const { p2p_country_list } = result.current; if (p2p_country_list) { expect(p2p_country_list).toEqual(mockQueryData.p2p_country_list); } From b54eca19afbaf4a4e96c5af86cd4210b6655505a Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Thu, 25 Apr 2024 16:26:21 +0400 Subject: [PATCH 09/17] fix: remove unnecessary change --- packages/components/src/components/dropdown/dropdown.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/components/src/components/dropdown/dropdown.scss b/packages/components/src/components/dropdown/dropdown.scss index b9d7d2296df5..8a676979506e 100644 --- a/packages/components/src/components/dropdown/dropdown.scss +++ b/packages/components/src/components/dropdown/dropdown.scss @@ -274,8 +274,7 @@ width: 100%; &:not(.cfd-personal-details-modal__form *):not(.trade-container__multiplier-dropdown):not( - .dc-dropdown--left - ):not(.contract-type-info__dropdown) { + .dc-dropdown--left):not(.contract-type-info__dropdown) { margin-top: unset; } From 2791f8a9931a8d5729727b7408f0728584d3b18e Mon Sep 17 00:00:00 2001 From: Farrah Mae Ochoa Date: Mon, 6 May 2024 21:34:13 +0400 Subject: [PATCH 10/17] fix: subtasks --- .../block-selector/block-selector.tsx | 5 +- .../ad-cancel-modal/ad-cancel-modal.scss | 5 ++ .../ad-cancel-modal/ad-cancel-modal.tsx | 2 +- .../modals/ad-cancel-modal/index.ts | 1 + .../ad-create-edit-error-modal.tsx | 12 ++++- .../modals/error-modal/error-modal.scss | 2 +- .../modals/error-modal/error-modal.tsx | 6 +-- .../preferred-countries-modal-body.tsx | 6 +-- .../preferred-countries-modal-footer.tsx | 17 ++++-- .../preferred-countries-modal.scss | 2 + .../preferred-countries-modal.tsx | 8 +-- .../ad-conditions-section.tsx | 53 +++++++++++++++++-- .../ad-form-controller/ad-form-controller.tsx | 4 +- .../ad-progress-bar/ad-progress-bar.tsx | 1 + .../src/pages/my-ads/ad-wizard/ad-wizard.tsx | 2 +- .../copy-advert-form/copy-advert-form.scss | 5 ++ .../copy-advert-form/copy-advert-form.tsx | 52 ++++++++++++++++++ .../p2p/src/pages/my-ads/create-ad-form.jsx | 2 + packages/p2p/src/pages/my-ads/create-ad.jsx | 3 -- .../p2p/src/pages/my-ads/edit-ad-form.jsx | 5 +- .../preferred-countries-selector.scss | 5 ++ .../preferred-countries-selector.tsx | 39 ++++++++------ packages/p2p/src/stores/my-ads-store.js | 5 +- 23 files changed, 191 insertions(+), 51 deletions(-) create mode 100644 packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.scss diff --git a/packages/p2p/src/components/block-selector/block-selector.tsx b/packages/p2p/src/components/block-selector/block-selector.tsx index 6c2115be3801..9990227cf655 100644 --- a/packages/p2p/src/components/block-selector/block-selector.tsx +++ b/packages/p2p/src/components/block-selector/block-selector.tsx @@ -13,7 +13,7 @@ type TBlockSelectorProps = { label: string; onSelect: (value: number) => void; options: TBlockSelectorOptionProps[]; - tooltip_info: string; + tooltip_info: React.ReactNode; value: number; }; @@ -53,7 +53,8 @@ const BlockSelector = ({ label, onSelect, options, tooltip_info, value }: TBlock showModal({ key: 'ErrorModal', props: { - error_message: localize(tooltip_info), + has_close_icon: false, + error_message: tooltip_info, error_modal_title: localize(label), }, }); diff --git a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.scss b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.scss new file mode 100644 index 000000000000..7b7af966b282 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.scss @@ -0,0 +1,5 @@ +.dc-modal__container_ad-cancel-modal { + .dc-modal-body { + padding: 0.8rem 2.4rem; + } +} diff --git a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx index 61c3e6f7a00c..481ea8023b50 100644 --- a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/ad-cancel-modal.tsx @@ -14,7 +14,7 @@ const AdCancelModal = ({ confirm_label, message, onConfirm, title }: TAdCancelMo const { hideModal, is_modal_open } = useModalManagerContext(); return ( - + {message} diff --git a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts index e18602925603..e1a4cd5baa4e 100644 --- a/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts +++ b/packages/p2p/src/components/modal-manager/modals/ad-cancel-modal/index.ts @@ -1,3 +1,4 @@ import AdCancelModal from './ad-cancel-modal'; +import './ad-cancel-modal.scss'; export default AdCancelModal; diff --git a/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/ad-create-edit-error-modal.tsx b/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/ad-create-edit-error-modal.tsx index ddec70c0755a..bd45e2f70eda 100644 --- a/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/ad-create-edit-error-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/ad-create-edit-error-modal.tsx @@ -10,9 +10,10 @@ import { generateErrorDialogBody, generateErrorDialogTitle } from 'Utils/adverts type TAdCreateEditErrorModalProps = { ad_type?: string; + onUpdateAd?: () => void; }; -const AdCreateEditErrorModal = ({ ad_type = ads.CREATE }: TAdCreateEditErrorModalProps) => { +const AdCreateEditErrorModal = ({ ad_type = ads.CREATE, onUpdateAd }: TAdCreateEditErrorModalProps) => { const { my_ads_store } = useStores(); const { hideModal, is_modal_open } = useModalManagerContext(); const { api_error_message, edit_ad_form_error, error_code } = my_ads_store; @@ -38,7 +39,14 @@ const AdCreateEditErrorModal = ({ ad_type = ads.CREATE }: TAdCreateEditErrorModa - diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss index e41662b248f8..453ec8b00859 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss @@ -39,6 +39,8 @@ display: flex; flex-direction: column; justify-content: center; + word-wrap: break-word; + margin: 0 2rem; } &__body { diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx index 0efa62dd9de0..81be7fec5d31 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx @@ -45,13 +45,12 @@ const PreferredCountriesModal = ({ country_list, eligible_countries, onApply }: {!search_value && ( { setSelectedCountries(eligible_countries); }} onApply={onApplySelectedCountries} + selected_countries={selected_countries} /> )} @@ -72,11 +71,12 @@ const PreferredCountriesModal = ({ country_list, eligible_countries, onApply }: } renderPageFooterChildren={() => ( { setSelectedCountries(eligible_countries); }} onApply={onApplySelectedCountries} + selected_countries={selected_countries} /> )} > diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx index 8b0f9021ca04..299deba6e938 100644 --- a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx @@ -13,10 +13,20 @@ import CreateAdSummary from '../create-ad-summary.jsx'; type TAdConditionsSection = { action: string; country_list: TCountryListProps; + goToFirstStep: () => void; + is_form_dirty: boolean; }; -const AdConditionsSection = ({ action, country_list, ...props }: TAdConditionsSection) => { +const AdConditionsSection = ({ + action, + country_list, + is_form_dirty, + goToFirstStep, + ...props +}: TAdConditionsSection) => { + const [has_min_join_days_changed, setHasMinJoinDaysChange] = React.useState(false); + const [has_min_completion_rate_changed, setHasMinCompletionRateChanged] = React.useState(false); const { showModal } = useModalManagerContext(); - const { errors, isSubmitting, isValid, values } = useFormikContext(); + const { dirty, errors, values } = useFormikContext(); const { my_ads_store } = useStores(); const { min_completion_rate, min_join_days } = my_ads_store.p2p_advert_information; const joining_days = [ @@ -31,11 +41,27 @@ const AdConditionsSection = ({ action, country_list, ...props }: TAdConditionsSe ]; const setJoiningDays = (value: number) => { my_ads_store.setMinJoinDays(value); + setHasMinJoinDaysChange(value !== min_join_days); }; const setMinCompletionRate = (value: number) => { my_ads_store.setMinCompletionRate(value); + setHasMinCompletionRateChanged(value !== min_completion_rate); }; + React.useEffect(() => { + if (my_ads_store.error_code) { + showModal({ + key: 'AdCreateEditErrorModal', + props: { + onUpdateAd: () => { + goToFirstStep(); + my_ads_store.setApiErrorCode(null); + }, + }, + }); + } + }, [my_ads_store.error_code]); + return ( <> + } value={min_join_days} /> + + + + + + + + } value={min_completion_rate} />
@@ -84,7 +121,13 @@ const AdConditionsSection = ({ action, country_list, ...props }: TAdConditionsSe />
- + ); }; diff --git a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx index ecfef4b3446a..d8e0f24c4e72 100644 --- a/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.tsx @@ -9,6 +9,7 @@ type TAdFormControllerProps = { goToNextStep: () => void; goToPreviousStep: () => void; is_next_btn_disabled: boolean; + is_save_btn_disabled: boolean; onCancel?: () => void; }; @@ -19,6 +20,7 @@ const AdFormController = ({ goToNextStep, goToPreviousStep, is_next_btn_disabled, + is_save_btn_disabled, onCancel, }: TAdFormControllerProps) => { const post_btn_text = @@ -56,7 +58,7 @@ const AdFormController = ({ ) : ( - )} diff --git a/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx b/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx index 7647fd97c547..617c8008c5c8 100644 --- a/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx +++ b/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx @@ -43,6 +43,7 @@ const AdProgressBar = ({ current_step, steps }: TAdProgressBar) => { textAnchor='middle' fontSize='var(--text-size-xs)' fontWeight='var(--text-weight-bold)' + fill='var(--text-prominent)' > {current_step + 1} / {steps.length}
diff --git a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx index a132f880330a..336d219aa431 100644 --- a/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx @@ -79,7 +79,7 @@ const AdWizard = ({ rate_type={rate_type} /> - + ); }; diff --git a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss index 5a4bb8d6e3a1..420301e5397a 100644 --- a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss +++ b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss @@ -34,6 +34,11 @@ } } + &__list { + list-style: disc; + margin-left: 1.5rem; + } + &__dropdown { &-display { border: 0; diff --git a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx index f4291cd261dd..399915542d02 100644 --- a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx +++ b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx @@ -29,6 +29,9 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { contact_info, description, amount_display, + eligible_countries, + min_completion_rate, + min_join_days, order_expiry_period, payment_method_details, payment_method_names, @@ -68,6 +71,10 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { return rate_display; }; + const eligible_countries_display = + eligible_countries?.length === 1 ? eligible_countries[0] : eligible_countries?.length; + const has_counterparty_conditions = min_join_days > 0 || min_completion_rate > 0 || eligible_countries?.length > 0; + React.useEffect(() => { if (type === buy_sell.SELL) { if (payment_method_details) { @@ -81,9 +88,15 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { }); } + my_ads_store.setMinJoinDays(min_join_days); + my_ads_store.setMinCompletionRate(min_completion_rate); + return () => { my_ads_store.payment_method_names = []; my_ads_store.payment_method_ids = []; + my_ads_store.setMinJoinDays(0); + my_ads_store.setMinCompletionRate(0); + my_ads_store.setP2pAdvertInformation({}); }; }, []); @@ -95,8 +108,11 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { contact_info, default_advert_description: description, float_rate_offset_limit: float_rate_offset_limit_string, + eligible_countries, max_transaction: '', min_transaction: '', + min_completion_rate, + min_join_days, offer_amount: amount_display, order_completion_time: order_expiry_period > 3600 ? '3600' : order_expiry_period.toString(), payment_method_names, @@ -260,6 +276,42 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { {payment_method_names.join(', ')} + {has_counterparty_conditions && ( + <> + + + + + {min_join_days > 0 && ( +
  • + ]} + values={{ min_join_days }} + /> +
  • + )} + {min_completion_rate > 0 && ( +
  • + ]} + values={{ min_completion_rate }} + /> +
  • + )} + {eligible_countries?.length > 0 && ( +
  • + ]} + values={{ eligible_countries_display }} + /> +
  • + )} +
    + + )}
    diff --git a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx index 0753c1c82530..204fd2dcf72d 100644 --- a/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx @@ -31,7 +31,10 @@ const PreferredCountriesModalBody = ({ const onClearSearch = () => { setSearchValue(''); - setSearchResults(country_list); + setSearchResults([ + ...country_list.filter(item => selected_countries.includes(item.value)), + ...country_list.filter(item => !selected_countries.includes(item.value)), + ]); }; const onSearch = e => { diff --git a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx index 3ba580f87492..d88a79cf9ef2 100644 --- a/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx @@ -126,7 +126,11 @@ const AdConditionsSection = ({ {...props} action={action} is_save_btn_disabled={ - !dirty && !is_form_dirty && !has_min_join_days_changed && !has_min_completion_rate_changed + !dirty && + !is_form_dirty && + !has_min_join_days_changed && + !has_min_completion_rate_changed && + my_ads_store.required_ad_type === my_ads_store.selected_ad_type } /> diff --git a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx index d886b980a34f..a0696f82460a 100644 --- a/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx @@ -233,7 +233,7 @@ const AdTypeSection = ({ } error={touched.contact_info && errors.contact_info} className='ad-type-section__field ad-type-section__field--textarea' - initial_character_count={general_store.contact_info.length} + initial_character_count={values.contact_info.length} required has_character_counter max_characters={300} @@ -258,7 +258,7 @@ const AdTypeSection = ({ } hint={localize('This information will be visible to everyone.')} className='ad-type-section__field ad-type-section__field--textarea' - initial_character_count={general_store.default_advert_description.length} + initial_character_count={values.default_advert_description.length} has_character_counter max_characters={300} onFocus={() => setFieldTouched('default_advert_description', true)} diff --git a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss index 420301e5397a..105ea1b4ed85 100644 --- a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss +++ b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss @@ -37,6 +37,10 @@ &__list { list-style: disc; margin-left: 1.5rem; + + @include mobile { + margin-bottom: 1.5rem; + } } &__dropdown { diff --git a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx index 399915542d02..1c44d496721a 100644 --- a/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx +++ b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.tsx @@ -2,23 +2,26 @@ import React from 'react'; import { Formik, Field, FieldProps, Form } from 'formik'; import { Button, InlineMessage, Input, Text, ThemedScrollbars } from '@deriv/components'; import { useP2PSettings } from '@deriv/hooks'; -import { useStore } from '@deriv/stores'; +import { observer, useStore } from '@deriv/stores'; import FloatingRate from 'Components/floating-rate'; import { Localize, localize } from 'Components/i18next'; +import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import { buy_sell } from 'Constants/buy-sell'; import { ad_type } from 'Constants/floating-rate'; import OrderTimeSelection from 'Pages/my-ads/order-time-selection'; import { useStores } from 'Stores'; -import { TAdvertProps } from 'Types'; +import { TAdvertProps, TCountryListProps } from 'Types'; import { getInlineTextSize } from 'Utils/responsive'; import CopyAdvertFormTrailingIcon from './copy-advert-from-trailing-icon'; type TCopyAdvertFormProps = { advert: TAdvertProps; + country_list: TCountryListProps; onCancel: () => void; }; -const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { +const CopyAdvertForm = ({ advert, country_list, onCancel }: TCopyAdvertFormProps) => { + const { showModal } = useModalManagerContext(); const { client: { currency, local_currency_config }, ui: { is_desktop }, @@ -71,8 +74,16 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { return rate_display; }; - const eligible_countries_display = - eligible_countries?.length === 1 ? eligible_countries[0] : eligible_countries?.length; + const getEligibleCountriesDisplay = () => { + if (eligible_countries?.length === 1) { + return country_list[eligible_countries[0]]?.country_name; + } else if (eligible_countries?.length === Object.keys(country_list)?.length) { + return localize('All'); + } + + return eligible_countries?.length; + }; + const has_counterparty_conditions = min_join_days > 0 || min_completion_rate > 0 || eligible_countries?.length > 0; React.useEffect(() => { @@ -100,6 +111,19 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { }; }, []); + React.useEffect(() => { + if (my_ads_store.error_code) { + showModal({ + key: 'AdCreateEditErrorModal', + props: { + onUpdateAd: () => { + my_ads_store.setApiErrorCode(null); + }, + }, + }); + } + }, [my_ads_store.error_code]); + return (
    { ]} - values={{ eligible_countries_display }} + values={{ + eligible_countries_display: getEligibleCountriesDisplay(), + }} /> )} @@ -329,4 +355,4 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { ); }; -export default CopyAdvertForm; +export default observer(CopyAdvertForm); diff --git a/packages/p2p/src/pages/my-ads/create-ad-form.jsx b/packages/p2p/src/pages/my-ads/create-ad-form.jsx index c00455c63b68..f7a811c23d6c 100644 --- a/packages/p2p/src/pages/my-ads/create-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad-form.jsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { Formik, Form } from 'formik'; import { Div100vhContainer, ThemedScrollbars } from '@deriv/components'; -import { useP2PCountryList, useP2PSettings } from '@deriv/hooks'; +import { useP2PSettings } from '@deriv/hooks'; import { isMobile } from '@deriv/shared'; import { observer } from '@deriv/stores'; import { buy_sell } from 'Constants/buy-sell'; @@ -18,7 +18,7 @@ const CreateAdFormWrapper = ({ children }) => { return children; }; -const CreateAdForm = () => { +const CreateAdForm = ({ country_list }) => { const { buy_sell_store, general_store, my_ads_store, my_profile_store } = useStores(); const { p2p_settings: { @@ -28,7 +28,6 @@ const CreateAdForm = () => { rate_type, }, } = useP2PSettings(); - const { p2p_country_list = {} } = useP2PCountryList(); const { useRegisterModalProps } = useModalManagerContext(); const steps = [ { header: { title: 'Set ad type and amount' } }, @@ -77,7 +76,7 @@ const CreateAdForm = () => { initialValues={{ contact_info: general_store.contact_info, default_advert_description: general_store.default_advert_description, - eligible_countries: p2p_country_list ? Object.keys(p2p_country_list) : [], + eligible_countries: country_list ? Object.keys(country_list) : [], float_rate_offset_limit: float_rate_offset_limit_string, max_transaction: '', min_transaction: '', @@ -101,7 +100,7 @@ const CreateAdForm = () => { > { my_ads_store.setShowAdForm(false); diff --git a/packages/p2p/src/pages/my-ads/create-ad.jsx b/packages/p2p/src/pages/my-ads/create-ad.jsx index c7a8e7b77cef..c00fd63c1584 100644 --- a/packages/p2p/src/pages/my-ads/create-ad.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad.jsx @@ -5,7 +5,7 @@ import CopyAdvertForm from 'Pages/my-ads/copy-advert-form'; import CreateAdForm from 'Pages/my-ads/create-ad-form'; import { useStores } from 'Stores'; -const CreateAd = () => { +const CreateAd = ({ country_list }) => { const { my_ads_store } = useStores(); const { is_form_loading, @@ -27,9 +27,9 @@ const CreateAd = () => { return ( {should_copy_advert ? ( - + ) : ( - + )} ); diff --git a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx index 4b29c1fcd933..dcd92e417be8 100644 --- a/packages/p2p/src/pages/my-ads/edit-ad-form.jsx +++ b/packages/p2p/src/pages/my-ads/edit-ad-form.jsx @@ -1,15 +1,12 @@ import * as React from 'react'; import { Formik, Form } from 'formik'; -import { Button, Div100vhContainer, Modal, Text, ThemedScrollbars } from '@deriv/components'; -import { useP2PCountryList, useP2PSettings } from '@deriv/hooks'; +import { Div100vhContainer, ThemedScrollbars } from '@deriv/components'; +import { useP2PSettings } from '@deriv/hooks'; import { isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; -import { localize } from 'Components/i18next'; -import { api_error_codes } from 'Constants/api-error-codes'; import { buy_sell } from 'Constants/buy-sell'; import { useStores } from 'Stores'; import { ad_type } from 'Constants/floating-rate'; -import { generateErrorDialogTitle, generateErrorDialogBody } from 'Utils/adverts'; import { useModalManagerContext } from 'Components/modal-manager/modal-manager-context'; import AdWizard from './ad-wizard'; import './edit-ad-form.scss'; @@ -22,7 +19,7 @@ const EditAdFormWrapper = ({ children }) => { return children; }; -const EditAdForm = () => { +const EditAdForm = ({ country_list }) => { const { my_ads_store, my_profile_store } = useStores(); const steps = [ { header: { title: 'Edit ad type and amount' } }, @@ -49,7 +46,6 @@ const EditAdForm = () => { const is_buy_advert = type === buy_sell.BUY; const [selected_methods, setSelectedMethods] = React.useState([]); const { useRegisterModalProps } = useModalManagerContext(); - const { p2p_country_list = {} } = useP2PCountryList(); const { p2p_settings } = useP2PSettings(); // when editing payment methods in creating an ad, once user declines to save their payment method, flow is to close all add payment method modals @@ -70,13 +66,8 @@ const EditAdForm = () => { return rate_display; }; - const is_api_error = [api_error_codes.ADVERT_SAME_LIMITS, api_error_codes.DUPLICATE_ADVERT].includes( - my_ads_store.error_code - ); - React.useEffect(() => { my_profile_store.getAdvertiserPaymentMethods(); - my_ads_store.setIsEditAdErrorModalVisible(false); my_ads_store.setEditAdFormError(''); if (payment_method_names && !payment_method_details) { @@ -138,7 +129,7 @@ const EditAdForm = () => { { my_ads_store.setShowEditAdForm(false); @@ -153,28 +144,6 @@ const EditAdForm = () => { ); }} - - - - {generateErrorDialogBody(my_ads_store.error_code, my_ads_store.edit_ad_form_error)} - - - -