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/components/src/components/dropdown/dropdown.scss b/packages/components/src/components/dropdown/dropdown.scss index 86595f7dee11..8a676979506e 100644 --- a/packages/components/src/components/dropdown/dropdown.scss +++ b/packages/components/src/components/dropdown/dropdown.scss @@ -273,7 +273,8 @@ min-width: 15rem; width: 100%; - &:not(.cfd-personal-details-modal__form *):not(.trade-container__multiplier-dropdown):not(.dc-dropdown--left):not(.contract-type-info__dropdown) { + &:not(.cfd-personal-details-modal__form *):not(.trade-container__multiplier-dropdown):not( + .dc-dropdown--left):not(.contract-type-info__dropdown) { margin-top: unset; } diff --git a/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx b/packages/hooks/src/__tests__/useP2PCountryList.spec.tsx new file mode 100644 index 000000000000..5db65047eda1 --- /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.p2p_country_list).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; + 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 0b78032845dc..3fd0a50b27f4 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -54,6 +54,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 new file mode 100644 index 000000000000..95c92ff605ac --- /dev/null +++ b/packages/p2p/src/components/block-selector/block-selector.scss @@ -0,0 +1,30 @@ +.block-selector { + margin: 2rem 0; + + &__label { + display: flex; + align-items: center; + column-gap: 1rem; + } + + &__options { + display: flex; + column-gap: 1rem; + + @include mobile { + justify-content: space-between; + } + } + + &__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..7ef98b58b02a --- /dev/null +++ b/packages/p2p/src/components/block-selector/block-selector.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import classNames from 'classnames'; +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; + name: string; + value: number; +}; +type TBlockSelectorProps = { + label: string; + onSelect: (value: number) => void; + options: TBlockSelectorOptionProps[]; + tooltip_info: React.ReactNode; + 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 { showModal } = useModalManagerContext(); + 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); + }; + + React.useEffect(() => { + onSelect(value); + }, []); + + return ( +
+
+ + + + { + showModal({ + key: 'ErrorModal', + props: { + has_close_icon: false, + error_message: tooltip_info, + error_modal_title: localize(label), + }, + }); + }} + /> +
+
+ {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/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/__tests__/ad-create-edit-error-modal.spec.tsx b/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/__tests__/ad-create-edit-error-modal.spec.tsx index e4d5f5a21944..880863d8cd2c 100644 --- a/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/__tests__/ad-create-edit-error-modal.spec.tsx +++ b/packages/p2p/src/components/modal-manager/modals/ad-create-edit-error-modal/__tests__/ad-create-edit-error-modal.spec.tsx @@ -47,7 +47,7 @@ describe('', () => { it('should display the ok button when there is no api error', () => { render(); - const ok_button = screen.getByRole('button', { name: 'Ok' }); + const ok_button = screen.getByRole('button', { name: 'OK' }); expect(ok_button).toBeInTheDocument(); }); it('should display the update ad button and "You already have an ad with this range" when there is api error', () => { @@ -67,7 +67,7 @@ describe('', () => { it('should close the modal on clicking update ad/ok button', () => { render(); - const ok_button = screen.getByRole('button', { name: 'Ok' }); + const ok_button = screen.getByRole('button', { name: 'OK' }); expect(ok_button).toBeInTheDocument(); userEvent.click(ok_button); expect(mock_modal_manager.hideModal).toHaveBeenCalledTimes(1); 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..7f17507fe936 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; @@ -37,8 +38,15 @@ const AdCreateEditErrorModal = ({ ad_type = ads.CREATE }: TAdCreateEditErrorModa 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/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-body.tsx b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx new file mode 100644 index 000000000000..01ae5fd3cc1b --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-body.tsx @@ -0,0 +1,121 @@ +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 => eligible_countries.includes(item.value)), + ...country_list.filter(item => !eligible_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 ( + <> + } + maxLength={50} + 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..ee72b8b9eb93 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal-footer.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Button } from '@deriv/components'; +import { Localize } from 'Components/i18next'; + +type TPreferredCountriesModalFooterProps = { + eligible_countries: string[]; + onClear: () => void; + onApply: () => void; + selected_countries: string[]; +}; + +const PreferredCountriesModalFooter = ({ + eligible_countries, + onClear, + onApply, + selected_countries, +}: TPreferredCountriesModalFooterProps) => { + const is_clear_btn_disabled = selected_countries?.length === 0; + const is_apply_btn_disabled = selected_countries?.length === 0 || selected_countries === eligible_countries; + + 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 new file mode 100644 index 000000000000..453ec8b00859 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.scss @@ -0,0 +1,67 @@ +.preferred-countries-modal { + &__search-field { + margin-bottom: 0; + + &--icon { + margin-left: 1.8rem !important; + } + + input { + 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; + word-wrap: break-word; + margin: 0 2rem; + } + + &__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 new file mode 100644 index 000000000000..81be7fec5d31 --- /dev/null +++ b/packages/p2p/src/components/modal-manager/modals/preferred-countries-modal/preferred-countries-modal.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +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 = { + country_list: { text: string; value: string }[]; + eligible_countries: string[]; + onApply?: (value: string[]) => void; +}; + +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()} + > + + + + {!search_value && ( + + { + setSelectedCountries(eligible_countries); + }} + onApply={onApplySelectedCountries} + selected_countries={selected_countries} + /> + + )} + + + + + + + } + renderPageFooterChildren={() => ( + { + setSelectedCountries(eligible_countries); + }} + onApply={onApplySelectedCountries} + selected_countries={selected_countries} + /> + )} + > + + + + + ); +}; +export default PreferredCountriesModal; diff --git a/packages/p2p/src/constants/modals.ts b/packages/p2p/src/constants/modals.ts index 415644725c85..e642b5b5efa0 100644 --- a/packages/p2p/src/constants/modals.ts +++ b/packages/p2p/src/constants/modals.ts @@ -185,6 +185,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/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/advertiser-page/advertiser-page.jsx b/packages/p2p/src/pages/advertiser-page/advertiser-page.jsx index f55324862fcb..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'; @@ -75,14 +75,22 @@ 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 = ''; + error_message = getEligibilityMessage(eligibility_status); + } + 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 +102,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/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 new file mode 100644 index 000000000000..2731f9ff8af6 --- /dev/null +++ b/packages/p2p/src/pages/buy-sell/buy-sell-row-action.tsx @@ -0,0 +1,75 @@ +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'; +import { getEligibilityMessage } from 'Utils/adverts'; + +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 onUnavailableClick = (eligibility_status: string[]) => { + showModal({ key: 'ErrorModal', props: { error_message: getEligibilityMessage(eligibility_status) } }); + }; + + 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 f176689893cc..94b898f5743b 100644 --- a/packages/p2p/src/pages/buy-sell/buy-sell-table.jsx +++ b/packages/p2p/src/pages/buy-sell/buy-sell-table.jsx @@ -113,6 +113,7 @@ const BuySellTable = ({ onScroll }) => { + )} 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..bb527f025c58 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.scss @@ -0,0 +1,15 @@ +.ad-conditions-section { + &__countries-label { + display: flex; + align-items: center; + column-gap: 1rem; + } + + &__label { + margin-top: 2rem; + } + + &__rate { + margin-top: 1.5rem; + } +} 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..862683593ffa --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-conditions-section/ad-conditions-section.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { FormikValues, useFormikContext } from 'formik'; +import { Icon, Text } from '@deriv/components'; +import { observer } from '@deriv/stores'; +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'; + +type TAdConditionsSection = { + action: string; + country_list: TCountryListProps; + goToFirstStep: () => void; + is_form_dirty: boolean; +}; +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 { dirty, errors, values } = useFormikContext(); + const { my_ads_store } = useStores(); + const { min_completion_rate = 0, min_join_days = 0 } = 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); + 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} + /> +
+ + + + { + 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'), + has_close_icon: false, + }, + }); + }} + /> +
+ + + + ); +}; + +export default observer(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..27d97fb231c5 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-form-controller/ad-form-controller.scss @@ -0,0 +1,16 @@ +.ad-form-controller { + display: flex; + justify-content: flex-end; + column-gap: 0.8rem; + border-top: 1px solid var(--general-section-1); + padding: 2rem 0; + + @include desktop { + margin-top: 5rem; + } + + @include mobile { + 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 new file mode 100644 index 000000000000..d8e0f24c4e72 --- /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; + goToNextStep: () => void; + goToPreviousStep: () => void; + is_next_btn_disabled: boolean; + is_save_btn_disabled: boolean; + onCancel?: () => void; +}; + +const AdFormController = ({ + action, + getCurrentStep, + getTotalSteps, + goToNextStep, + goToPreviousStep, + is_next_btn_disabled, + is_save_btn_disabled, + onCancel, +}: TAdFormControllerProps) => { + const post_btn_text = + action === 'edit' ? : ; + + return ( +
+ {getCurrentStep() === 1 && onCancel && ( + + )} + {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..c934b5f97b52 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-payment-details-section/ad-payment-details-section.tsx @@ -0,0 +1,74 @@ +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 { 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'; + +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]); + + 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-progress-bar/ad-progress-bar.tsx b/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx new file mode 100644 index 000000000000..617c8008c5c8 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-progress-bar/ad-progress-bar.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +type TStep = { header: { title: string }; sub_step_count: number }; +type TAdProgressBar = { + current_step: number; + steps: TStep[]; +}; + +const AdProgressBar = ({ current_step, steps }: TAdProgressBar) => { + 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 new file mode 100644 index 000000000000..00ed1e05c973 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.scss @@ -0,0 +1,107 @@ +.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: 2rem 0 0; + width: 67.2rem; + + @include mobile { + height: 9.8rem; + width: auto; + } + + .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 { + 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 new file mode 100644 index 000000000000..a0696f82460a --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-type-section/ad-type-section.tsx @@ -0,0 +1,274 @@ +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'; +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, + 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 } = useStores(); + const { dirty, errors, handleChange, isValid, setFieldValue, setFieldTouched, touched, values } = + useFormikContext(); + const is_edit = action === 'edit'; + const is_next_btn_disabled = is_edit ? !isValid : !dirty || !isValid; + const onChangeAdTypeHandler = (user_input: string) => { + if (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) { + if (dirty || is_form_dirty) { + general_store.showModal({ + key: 'AdCancelModal', + props: { + confirm_label: localize("Don't cancel"), + message: localize('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: { + confirm_label: localize("Don't cancel"), + message: localize("If you choose to cancel, the details you've entered will be lost."), + onConfirm: () => { + my_ads_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 }) => ( + } + 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 + disabled={is_edit} + /> + )} + + + {({ field }) => + rate_type === ad_type.FLOAT ? ( + setFieldTouched('rate_type', true)} + offset={{ + upper_limit: parseInt(float_rate_offset_limit_string), + lower_limit: parseInt(float_rate_offset_limit_string) * -1, + }} + required + change_handler={e => { + my_ads_store.restrictDecimalPlace(e, handleChange); + }} + {...field} + /> + ) : ( + } + onChange={e => { + my_ads_store.restrictLength(e, handleChange); + }} + onFocus={() => setFieldTouched('rate_type', true)} + required + /> + ) + } + +
+
+ + {({ field }) => ( + } + onChange={e => { + my_ads_store.restrictLength(e, handleChange); + }} + onFocus={() => setFieldTouched('min_transaction', true)} + required + /> + )} + + + {({ field }) => ( + } + 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={values.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={values.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/ad-wizard/ad-wizard.scss b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss new file mode 100644 index 000000000000..a6342a2f3765 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.scss @@ -0,0 +1,50 @@ +.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; + border-radius: 0.4rem; + + &__step { + width: 24rem; + } + + &__steps { + 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 new file mode 100644 index 000000000000..336d219aa431 --- /dev/null +++ b/packages/p2p/src/pages/my-ads/ad-wizard/ad-wizard.tsx @@ -0,0 +1,87 @@ +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'; +import { TCountryListProps } from 'Types'; + +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, + 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); + + 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/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/copy-advert-form/copy-advert-form.scss b/packages/p2p/src/pages/my-ads/copy-advert-form/copy-advert-form.scss index 5a4bb8d6e3a1..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 @@ -34,6 +34,15 @@ } } + &__list { + list-style: disc; + margin-left: 1.5rem; + + @include mobile { + margin-bottom: 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..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 }, @@ -29,6 +32,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 +74,18 @@ const CopyAdvertForm = ({ advert, onCancel }: TCopyAdvertFormProps) => { return rate_display; }; + 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(() => { if (type === buy_sell.SELL) { if (payment_method_details) { @@ -81,12 +99,31 @@ 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({}); }; }, []); + React.useEffect(() => { + if (my_ads_store.error_code) { + showModal({ + key: 'AdCreateEditErrorModal', + props: { + onUpdateAd: () => { + my_ads_store.setApiErrorCode(null); + }, + }, + }); + } + }, [my_ads_store.error_code]); + return (
{ 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 +300,44 @@ 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: getEligibleCountriesDisplay(), + }} + /> +
  • + )} +
    + + )}
    - -
    + { + my_ads_store.setShowAdForm(false); + }} + rate_type={rate_type} + steps={steps} + /> 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 88e0b62b7f0b..a15ec8fee654 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,10 +75,14 @@ } } - &__offer-amt { - &__sell_ad { - margin-top: 0 !important; - } + &__progress-bar { + width: 5.6rem; + height: 5.6rem; + border-radius: 50%; + border: 2px solid; + display: flex; + align-items: center; + justify-content: center; } &__dropdown { diff --git a/packages/p2p/src/pages/my-ads/create-ad.jsx b/packages/p2p/src/pages/my-ads/create-ad.jsx index fc8bf3f9524f..c00fd63c1584 100644 --- a/packages/p2p/src/pages/my-ads/create-ad.jsx +++ b/packages/p2p/src/pages/my-ads/create-ad.jsx @@ -1,13 +1,11 @@ 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 CopyAdvertForm from 'Pages/my-ads/copy-advert-form'; +import CreateAdForm from 'Pages/my-ads/create-ad-form'; import { useStores } from 'Stores'; -import CreateAdForm from './create-ad-form.jsx'; -const CreateAd = () => { +const CreateAd = ({ country_list }) => { const { my_ads_store } = useStores(); const { is_form_loading, @@ -28,11 +26,10 @@ const CreateAd = () => { } return ( - {should_copy_advert ? ( - + ) : ( - + )} ); 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 d6cdeb0d77ba..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,22 +1,14 @@ 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 { Formik, Form } from 'formik'; +import { Div100vhContainer, ThemedScrollbars } from '@deriv/components'; import { useP2PSettings } from '@deriv/hooks'; -import { formatMoney, isDesktop, isMobile } from '@deriv/shared'; +import { isMobile } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; -import { Localize, localize } from 'Components/i18next'; -import PageReturn from 'Components/page-return'; -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 AdWizard from './ad-wizard'; import './edit-ad-form.scss'; const EditAdFormWrapper = ({ children }) => { @@ -27,15 +19,19 @@ const EditAdFormWrapper = ({ children }) => { return children; }; -const EditAdForm = () => { - const { general_store, my_ads_store, my_profile_store } = useStores(); +const EditAdForm = ({ country_list }) => { + const { my_ads_store, my_profile_store } = useStores(); + 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, + eligible_countries, max_order_amount_display, min_order_amount_display, order_expiry_period, @@ -49,7 +45,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(); const { p2p_settings } = useP2PSettings(); @@ -71,44 +66,8 @@ 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: 'AdCancelModal', - props: { - confirm_label: localize("Don't cancel"), - message: localize('If you choose to cancel, the edited details will be lost.'), - onConfirm: () => my_ads_store.setShowEditAdForm(false), - title: localize('Cancel your edits?'), - }, - }); - } 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 - ); - React.useEffect(() => { my_profile_store.getAdvertiserPaymentMethods(); - my_ads_store.setIsEditAdErrorModalVisible(false); my_ads_store.setEditAdFormError(''); if (payment_method_names && !payment_method_details) { @@ -128,29 +87,25 @@ 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 = []; + my_ads_store.setMinJoinDays(0); + my_ads_store.setMinCompletionRate(0); + my_ads_store.setP2pAdvertInformation({}); }; // 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 ? ( - - ) : ( - - )} - -
    - -
    -
    - - -
    + { + my_ads_store.setShowEditAdForm(false); + }} + rate_type={p2p_settings.rate_type} + steps={steps} + />
    @@ -457,28 +144,6 @@ const EditAdForm = () => { ); }} - - - - {generateErrorDialogBody(my_ads_store.error_code, my_ads_store.edit_ad_form_error)} - - - -