diff --git a/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx b/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx index 14f34d6247d5..13da8b4a6d2a 100644 --- a/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx +++ b/packages/cashier/src/pages/p2p-cashier/p2p-cashier.jsx @@ -11,6 +11,7 @@ import { get, init, timePromise } from '_common/server_time'; /* P2P will use the same websocket connection as Deriv/Binary, we need to pass it as a prop */ const P2PCashier = ({ currency, + current_focus, history, is_dark_mode_on, is_logging_in, @@ -22,6 +23,8 @@ const P2PCashier = ({ platform, residence, setNotificationCount, + setCurrentFocus, + balance, setOnRemount, }) => { const [order_id, setOrderId] = React.useState(null); @@ -75,6 +78,7 @@ const P2PCashier = ({ return ( ); }; P2PCashier.propTypes = { + balance: PropTypes.string, currency: PropTypes.string, + current_focus: PropTypes.string, history: PropTypes.object, is_dark_mode_on: PropTypes.bool, is_logging_in: PropTypes.bool, @@ -106,10 +114,12 @@ P2PCashier.propTypes = { platform: PropTypes.any, residence: PropTypes.string, setNotificationCount: PropTypes.func, + setCurrentFocus: PropTypes.func, }; export default withRouter( connect(({ client, common, modules, ui }) => ({ + balance: client.balance, currency: client.currency, local_currency_config: client.local_currency_config, loginid: client.loginid, @@ -121,5 +131,7 @@ export default withRouter( setNotificationCount: modules.cashier.general_store.setNotificationCount, setOnRemount: modules.cashier.general_store.setOnRemount, is_mobile: ui.is_mobile, + setCurrentFocus: ui.setCurrentFocus, + current_focus: ui.current_focus, }))(P2PCashier) ); diff --git a/packages/components/src/components/input-field/increment-buttons.jsx b/packages/components/src/components/input-field/increment-buttons.jsx index 19dcb17e9b42..a22fd9a10bb4 100644 --- a/packages/components/src/components/input-field/increment-buttons.jsx +++ b/packages/components/src/components/input-field/increment-buttons.jsx @@ -11,6 +11,7 @@ const IncrementButtons = ({ min_is_disabled, is_incrementable_on_long_press, onLongPressEnd, + type, }) => { const interval_ref = React.useRef(); const timeout_ref = React.useRef(); @@ -56,6 +57,7 @@ const IncrementButtons = ({ onClick={incrementValue} tabIndex='-1' aria-label={'Increment value'} + type={type} {...getPressEvents(incrementValue)} > { const [local_value, setLocalValue] = React.useState(); - const Icon = icon; const has_error = error_messages && !!error_messages.length; const max_is_disabled = max_value && (+value >= +max_value || +local_value >= +max_value); @@ -135,22 +140,21 @@ const InputField = ({ if (long_press_step) { const increase_percentage = Math.min(long_press_step, Math.max(long_press_step, 10)) / 10; const increase = (value * increase_percentage) / 100; - const new_value = parseFloat(+(current_value || 0)) + Math.abs(increase); + const new_value = parseFloat(current_value || 0) + Math.abs(increase); increment_value = parseFloat(getClampedValue(new_value)).toFixed(decimal_places); } else if (is_crypto || (!currency && is_float)) { - const new_value = parseFloat(+(current_value || 0)) + parseFloat(1 * 10 ** (0 - decimal_places)); - increment_value = parseFloat(new_value).toFixed(decimal_places); + const new_value = + parseFloat(current_value || 0) + parseFloat(1 * 10 ** (0 - (decimal_point_change || decimal_places))); + increment_value = parseFloat(new_value).toFixed(decimal_point_change || decimal_places); } else { - increment_value = parseFloat(+(current_value || 0) + 1).toFixed(decimal_places); + increment_value = parseFloat((current_value || 0) + 1).toFixed(decimal_places); } - updateValue(increment_value, !!long_press_step); }; const calculateDecrementedValue = long_press_step => { let decrement_value; - const current_value = local_value || value; const decimal_places = current_value ? getDecimals(current_value) : 0; @@ -159,30 +163,38 @@ const InputField = ({ if (long_press_step) { const decrease_percentage = Math.min(long_press_step, Math.max(long_press_step, 10)) / 10; const decrease = (value * decrease_percentage) / 100; - const new_value = parseFloat(+(current_value || 0)) - Math.abs(decrease); + const new_value = parseFloat(current_value || 0) - Math.abs(decrease); decrement_value = parseFloat(getClampedValue(new_value)).toFixed(decimal_places); } else if (is_crypto || (!currency && is_float)) { - const new_value = parseFloat(+(current_value || 0)) - parseFloat(1 * 10 ** (0 - decimal_places)); - decrement_value = parseFloat(new_value).toFixed(decimal_places); + const new_value = + parseFloat(current_value || 0) - parseFloat(1 * 10 ** (0 - (decimal_point_change || decimal_places))); + decrement_value = parseFloat(new_value).toFixed(decimal_point_change || decimal_places); } else { - decrement_value = parseFloat(+(current_value || 0) - 1).toFixed(decimal_places); + decrement_value = parseFloat((current_value || 0) - 1).toFixed(decimal_places); } return decrement_value; }; const decrementValue = (ev, long_press_step) => { - if (!value || min_is_disabled) return; + if (min_is_disabled) { + return; + } const decrement_value = calculateDecrementedValue(long_press_step); - if (is_negative_disabled && decrement_value < 0) return; + if (is_negative_disabled && decrement_value < 0) { + return; + } updateValue(decrement_value, !!long_press_step); }; const updateValue = (new_value, is_long_press) => { - const formatted_value = format ? format(new_value) : new_value; + let formatted_value = format ? format(new_value) : new_value; if (is_long_press) { setLocalValue(formatted_value); } else { + if (is_signed && /^\d+/.test(formatted_value) && formatted_value > 0) { + formatted_value = `+${formatted_value}`; + } onChange({ target: { value: formatted_value, name } }); } }; @@ -221,8 +233,10 @@ const InputField = ({ { 'input--error': has_error }, classNameInput )} + classNameDynamicSuffix={classNameDynamicSuffix} classNameInlinePrefix={classNameInlinePrefix} data_tip={data_tip} + data_testid={data_testid} data_value={data_value} display_value={display_value} fractional_digits={fractional_digits} @@ -236,6 +250,7 @@ const InputField = ({ is_read_only={is_read_only} max_length={max_length} name={name} + onBlur={onBlur} onClick={onClick} onKeyPressed={onKeyPressed} placeholder={placeholder} @@ -257,6 +272,7 @@ const InputField = ({ decrementValue={decrementValue} onLongPressEnd={onLongPressEnd} is_incrementable_on_long_press={is_incrementable_on_long_press} + type={increment_button_type} /> ); @@ -279,9 +295,13 @@ const InputField = ({ )} {is_increment_input ? (
{increment_buttons} {input} @@ -318,9 +338,12 @@ InputField.propTypes = { className: PropTypes.string, classNameInlinePrefix: PropTypes.string, classNameInput: PropTypes.string, + classNameDynamicSuffix: PropTypes.string, classNamePrefix: PropTypes.string, + classNameWrapper: PropTypes.string, // CSS class for the component wrapper currency: PropTypes.string, current_focus: PropTypes.string, + decimal_point_change: PropTypes.number, // Specify which decimal point must be updated when the increment/decrement button is pressed error_messages: PropTypes.array, error_message_alignment: PropTypes.string, fractional_digits: PropTypes.number, @@ -338,9 +361,11 @@ InputField.propTypes = { is_read_only: PropTypes.bool, is_signed: PropTypes.bool, is_unit_at_right: PropTypes.bool, + increment_button_type: PropTypes.string, label: PropTypes.string, max_length: PropTypes.number, name: PropTypes.string, + onBlur: PropTypes.func, onChange: PropTypes.func, onClick: PropTypes.func, onClickInputWrapper: PropTypes.func, diff --git a/packages/components/src/components/input-field/input.jsx b/packages/components/src/components/input-field/input.jsx index e54f8624d3df..d79c6d1a7f50 100644 --- a/packages/components/src/components/input-field/input.jsx +++ b/packages/components/src/components/input-field/input.jsx @@ -9,6 +9,7 @@ const Input = ({ checked, className, classNameInlinePrefix, + classNameDynamicSuffix, current_focus, data_value, data_tip, @@ -24,12 +25,14 @@ const Input = ({ is_read_only, max_length, name, + onBlur, onClick, onKeyPressed, placeholder, required, setCurrentFocus, type, + data_testid, }) => { const ref = React.useRef(); React.useEffect(() => { @@ -38,7 +41,12 @@ const Input = ({ } }, [current_focus, name]); - const onBlur = () => setCurrentFocus(null); + const onBlurHandler = e => { + setCurrentFocus(null); + if (onBlur) { + onBlur(e); + } + }; const onFocus = () => setCurrentFocus(name); const onChange = e => { @@ -59,7 +67,7 @@ const Input = ({ }; return ( - +
{!!inline_prefix && (
- +
); }; @@ -107,6 +117,7 @@ Input.propTypes = { checked: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), className: PropTypes.string, classNameInlinePrefix: PropTypes.string, + classNameDynamicSuffix: PropTypes.string, current_focus: PropTypes.string, data_tip: PropTypes.string, data_value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), @@ -121,6 +132,7 @@ Input.propTypes = { is_read_only: PropTypes.bool, max_length: PropTypes.number, name: PropTypes.string, + onBlur: PropTypes.func, onClick: PropTypes.func, onKeyPressed: PropTypes.func, placeholder: PropTypes.string, diff --git a/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx b/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx index 30c84cecf054..8956765dcd31 100644 --- a/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx +++ b/packages/p2p/src/components/advertiser-page/advertiser-page-row.jsx @@ -6,10 +6,11 @@ import { observer } from 'mobx-react-lite'; import { useStores } from 'Stores'; import { buy_sell } from 'Constants/buy-sell'; import { localize, Localize } from 'Components/i18next'; +import { generateEffectiveRate } from 'Utils/format-value.js'; import './advertiser-page.scss'; const AdvertiserPageRow = ({ row: advert, showAdPopup }) => { - const { advertiser_page_store, buy_sell_store, general_store } = useStores(); + const { advertiser_page_store, buy_sell_store, floating_rate_store, general_store } = useStores(); const { currency } = general_store.client; const { local_currency, @@ -17,11 +18,21 @@ const AdvertiserPageRow = ({ row: advert, showAdPopup }) => { min_order_amount_limit_display, payment_method_names, price_display, + rate_type, + rate, } = advert; const is_buy_advert = advertiser_page_store.counterparty_type === buy_sell.BUY; const is_my_advert = advertiser_page_store.advertiser_details_id === general_store.advertiser_id; + const { display_effective_rate } = generateEffectiveRate({ + price: price_display, + rate_type, + rate, + local_currency, + exchange_rate: floating_rate_store.exchange_rate, + }); + const showAdForm = () => { buy_sell_store.setSelectedAdState(advert); showAdPopup(advert); @@ -42,7 +53,7 @@ const AdvertiserPageRow = ({ row: advert, showAdPopup }) => {
- {price_display} {local_currency} + {display_effective_rate} {local_currency}
@@ -89,7 +100,7 @@ const AdvertiserPageRow = ({ row: advert, showAdPopup }) => { {`${min_order_amount_limit_display}-${max_order_amount_limit_display} ${currency}`} - {price_display} {local_currency} + {display_effective_rate} {local_currency} diff --git a/packages/p2p/src/components/advertiser-page/advertiser-page.jsx b/packages/p2p/src/components/advertiser-page/advertiser-page.jsx index 69c189be511f..fb91883fdf69 100644 --- a/packages/p2p/src/components/advertiser-page/advertiser-page.jsx +++ b/packages/p2p/src/components/advertiser-page/advertiser-page.jsx @@ -7,6 +7,7 @@ import PropTypes from 'prop-types'; import PageReturn from 'Components/page-return/page-return.jsx'; import { Localize, localize } from 'Components/i18next'; import { buy_sell } from 'Constants/buy-sell'; +import RateChangeModal from 'Components/buy-sell/rate-change-modal.jsx'; import BuySellModal from 'Components/buy-sell/buy-sell-modal.jsx'; import UserAvatar from 'Components/user/user-avatar/user-avatar.jsx'; import { useStores } from 'Stores'; @@ -52,6 +53,7 @@ const AdvertiserPage = () => { return (
+ { const { general_store, order_store } = useStores(); - const { className, history, lang, order_id, server_time, websocket_api, setOnRemount } = props; + const { balance, className, history, lang, order_id, server_time, websocket_api, setOnRemount } = props; React.useEffect(() => { general_store.setAppProps(props); @@ -46,6 +46,10 @@ const App = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + React.useEffect(() => { + setLanguage(lang); + }, [lang]); + React.useEffect(() => { if (order_id) { general_store.redirectTo('orders'); @@ -53,6 +57,10 @@ const App = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [order_id]); + React.useEffect(() => { + general_store.setAccountBalance(balance); + }, [balance]); + React.useEffect(() => { setLanguage(lang); }, [lang]); @@ -75,11 +83,13 @@ App.propTypes = { loginid: PropTypes.string.isRequired, residence: PropTypes.string.isRequired, }), + balance: PropTypes.string, lang: PropTypes.string, modal_root_id: PropTypes.string.isRequired, order_id: PropTypes.string, setNotificationCount: PropTypes.func, websocket_api: PropTypes.object.isRequired, + setOnRemount: PropTypes.func, }; export default observer(App); diff --git a/packages/p2p/src/components/buy-sell/buy-sell-form-receive-amount.jsx b/packages/p2p/src/components/buy-sell/buy-sell-form-receive-amount.jsx index be849241713d..29f55f04b6c0 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-form-receive-amount.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-form-receive-amount.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { Text } from '@deriv/components'; import { getFormattedText } from '@deriv/shared'; import { Localize } from 'Components/i18next'; +import { roundOffDecimal } from 'Utils/format-value.js'; import { useStores } from 'Stores'; const BuySellFormReceiveAmount = () => { @@ -17,7 +18,10 @@ const BuySellFormReceiveAmount = () => { )} - {getFormattedText(buy_sell_store?.receive_amount, buy_sell_store?.advert?.local_currency)} + {getFormattedText( + roundOffDecimal(buy_sell_store?.receive_amount), + buy_sell_store?.advert?.local_currency + )} ); diff --git a/packages/p2p/src/components/buy-sell/buy-sell-form.jsx b/packages/p2p/src/components/buy-sell/buy-sell-form.jsx index ac75b8a88002..d257be0dc927 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-form.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-form.jsx @@ -1,20 +1,22 @@ +import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; import { Formik, Field, Form } from 'formik'; -import { Icon, Input, Text } from '@deriv/components'; -import { getRoundedNumber, getFormattedText, isDesktop, isMobile, useIsMounted } from '@deriv/shared'; +import { HintBox, Icon, Input, Text } from '@deriv/components'; +import { getRoundedNumber, isDesktop, isMobile, useIsMounted } from '@deriv/shared'; import { reaction } from 'mobx'; import { observer, Observer } from 'mobx-react-lite'; import { localize, Localize } from 'Components/i18next'; +import { ad_type } from 'Constants/floating-rate.js'; import { useStores } from 'Stores'; import BuySellFormReceiveAmount from './buy-sell-form-receive-amount.jsx'; import PaymentMethodCard from '../my-profile/payment-methods/payment-method-card/payment-method-card.jsx'; import { floatingPointValidator } from 'Utils/validations'; +import { generateEffectiveRate, setDecimalPlaces, roundOffDecimal, removeTrailingZeros } from 'Utils/format-value.js'; const BuySellForm = props => { const isMounted = useIsMounted(); - const { advertiser_page_store, buy_sell_store, my_profile_store } = useStores(); - + const { advertiser_page_store, buy_sell_store, floating_rate_store, general_store, my_profile_store } = useStores(); const [selected_methods, setSelectedMethods] = React.useState([]); buy_sell_store.setFormProps(props); @@ -28,13 +30,32 @@ const BuySellForm = props => { min_order_amount_limit_display, payment_method_names, price, + rate, + rate_type, } = buy_sell_store?.advert || {}; + const [input_amount, setInputAmount] = React.useState(min_order_amount_limit); + + const should_disable_field = + !buy_sell_store.is_buy_advert && + (parseFloat(general_store.balance) === 0 || + parseFloat(general_store.balance) < buy_sell_store.advert?.min_order_amount_limit); const style = { borderColor: 'var(--brand-secondary)', borderWidth: '2px', + cursor: should_disable_field ? 'not-allowed' : 'pointer', }; + const { effective_rate, display_effective_rate } = generateEffectiveRate({ + price, + rate_type, + rate, + local_currency, + exchange_rate: floating_rate_store.exchange_rate, + }); + + const calculated_rate = removeTrailingZeros(roundOffDecimal(effective_rate, setDecimalPlaces(effective_rate, 6))); + React.useEffect( () => { my_profile_store.setShouldShowAddPaymentMethodForm(false); @@ -46,13 +67,7 @@ const BuySellForm = props => { () => buy_sell_store.receive_amount, () => { if (isMobile() && typeof setPageFooterParent === 'function') { - setPageFooterParent( - - ); + setPageFooterParent(); } } ); @@ -62,7 +77,8 @@ const BuySellForm = props => { } advertiser_page_store.setFormErrorMessage(''); - buy_sell_store.setInitialReceiveAmount(); + buy_sell_store.setShowRateChangePopup(rate_type === ad_type.FLOAT); + buy_sell_store.setInitialReceiveAmount(calculated_rate); return () => { buy_sell_store.payment_method_ids = []; @@ -72,45 +88,64 @@ const BuySellForm = props => { [] // eslint-disable-line react-hooks/exhaustive-deps ); + React.useEffect(() => { + const receive_amount = input_amount * calculated_rate; + buy_sell_store.setReceiveAmount(receive_amount); + }, [input_amount, effective_rate]); + const onClickPaymentMethodCard = payment_method => { - if (!buy_sell_store.payment_method_ids.includes(payment_method.ID)) { - if (buy_sell_store.payment_method_ids.length < 3) { - buy_sell_store.payment_method_ids.push(payment_method.ID); - setSelectedMethods([...selected_methods, payment_method.ID]); + if (!should_disable_field) { + if (!buy_sell_store.payment_method_ids.includes(payment_method.ID)) { + if (buy_sell_store.payment_method_ids.length < 3) { + buy_sell_store.payment_method_ids.push(payment_method.ID); + setSelectedMethods([...selected_methods, payment_method.ID]); + } + } else { + buy_sell_store.payment_method_ids = buy_sell_store.payment_method_ids.filter( + payment_method_id => payment_method_id !== payment_method.ID + ); + setSelectedMethods(selected_methods.filter(i => i !== payment_method.ID)); } - } else { - buy_sell_store.payment_method_ids = buy_sell_store.payment_method_ids.filter( - payment_method_id => payment_method_id !== payment_method.ID - ); - setSelectedMethods(selected_methods.filter(i => i !== payment_method.ID)); } }; return ( - { - buy_sell_store.handleSubmit(() => isMounted(), ...args); - }} - > - {({ errors, isSubmitting, isValid, setFieldValue, submitForm, touched, values }) => { - buy_sell_store.form_props.setIsSubmitDisabled( - !isValid || - isSubmitting || - (buy_sell_store.is_sell_advert && payment_method_names && selected_methods.length < 1) - ); - buy_sell_store.form_props.setSubmitForm(submitForm); + + {rate_type === ad_type.FLOAT && !should_disable_field && ( +
+ + + + } + is_info + /> +
+ )} + buy_sell_store.handleSubmit(() => isMounted(), ...args)} + > + {({ errors, isSubmitting, isValid, setFieldValue, submitForm, touched, values }) => { + buy_sell_store.form_props.setIsSubmitDisabled( + !isValid || + isSubmitting || + (buy_sell_store.is_sell_advert && payment_method_names && selected_methods.length < 1) + ); + buy_sell_store.form_props.setSubmitForm(submitForm); - return ( - + return (
@@ -134,7 +169,7 @@ const BuySellForm = props => { /> - {getFormattedText(price, local_currency)} + {display_effective_rate} {local_currency}
@@ -205,146 +240,171 @@ const BuySellForm = props => {
{buy_sell_store.is_sell_advert && payment_method_names && ( -
- - - - - {my_profile_store.advertiser_has_payment_methods ? ( - - ) : ( - - )} - - - {() => ( -
- {payment_method_names - ?.map((add_payment_method, key) => { - const { - advertiser_payment_methods_list, - setSelectedPaymentMethodDisplayName, - setShouldShowAddPaymentMethodForm, - } = my_profile_store; - const matching_payment_methods = - advertiser_payment_methods_list.filter( - advertiser_payment_method => - advertiser_payment_method.display_name === - add_payment_method - ); - return matching_payment_methods.length > 0 ? ( - matching_payment_methods.map(payment_method => ( + +
+ + + + + {my_profile_store.advertiser_has_payment_methods ? ( + + ) : ( + + )} + + + {() => ( +
+ {payment_method_names + ?.map((add_payment_method, key) => { + const { + advertiser_payment_methods_list, + setSelectedPaymentMethodDisplayName, + setShouldShowAddPaymentMethodForm, + } = my_profile_store; + const matching_payment_methods = + advertiser_payment_methods_list.filter( + advertiser_payment_method => + advertiser_payment_method.display_name === + add_payment_method + ); + return matching_payment_methods.length > 0 ? ( + matching_payment_methods.map(payment_method => ( + + onClickPaymentMethodCard(payment_method) + } + payment_method={payment_method} + style={ + selected_methods.includes( + payment_method.ID + ) + ? style + : {} + } + disabled={should_disable_field} + /> + )) + ) : ( - onClickPaymentMethodCard(payment_method) - } - payment_method={payment_method} - style={ - selected_methods.includes(payment_method.ID) - ? style - : {} - } + onClickAdd={() => { + if (!should_disable_field) { + setSelectedPaymentMethodDisplayName( + add_payment_method + ); + setShouldShowAddPaymentMethodForm(true); + } + }} + disabled={should_disable_field} + style={{ + cursor: should_disable_field + ? 'not-allowed' + : 'pointer', + }} /> - )) - ) : ( - { - setSelectedPaymentMethodDisplayName( - add_payment_method - ); - setShouldShowAddPaymentMethodForm(true); - }} - /> - ); - }) - .sort(payment_method_card_node => - Array.isArray(payment_method_card_node) && - !payment_method_card_node[0].props?.is_add - ? -1 - : 1 - )} -
- )} -
-
+ ); + }) + .sort(payment_method_card_node => + Array.isArray(payment_method_card_node) && + !payment_method_card_node[0].props?.is_add + ? -1 + : 1 + )} +
+ )} +
+
+
+ )} -
- - {({ field }) => ( - - } - is_relative_hint - className='buy-sell__modal-field' - trailing_icon={ - - {buy_sell_store.account_currency} - - } - onKeyDown={event => { - if (!floatingPointValidator(event.key)) { - event.preventDefault(); +
+ + {localize('Enter {{transaction_type}} amount', { + transaction_type: buy_sell_store.is_buy_advert ? 'buy' : 'sell', + })} + +
+ + {({ field }) => ( + } - }} - onChange={event => { - if (event.target.value === '') { - setFieldValue('amount', ''); - buy_sell_store.setReceiveAmount(0); - } else { - const input_amount = getRoundedNumber( - event.target.value, - buy_sell_store.account_currency - ); - - setFieldValue('amount', getRoundedNumber(input_amount)); - buy_sell_store.setReceiveAmount( - getRoundedNumber( - input_amount * price, - buy_sell_store.account_currency - ) - ); + is_relative_hint + className='buy-sell__modal-field' + trailing_icon={ + + {buy_sell_store.account_currency} + } - }} - required - value={values.amount} - /> + onKeyDown={event => { + if (!floatingPointValidator(event.key)) { + event.preventDefault(); + } + }} + onChange={event => { + if (event.target.value === '') { + setFieldValue('amount', ''); + setInputAmount(0); + } else { + const amount = getRoundedNumber( + event.target.value, + buy_sell_store.account_currency + ); + setFieldValue('amount', amount); + setInputAmount(amount); + } + }} + required + value={values.amount} + disabled={should_disable_field} + /> + )} + + {isDesktop() && ( +
+ +
)} - - {isDesktop() && ( -
- -
- )} +
{buy_sell_store.is_sell_advert && ( @@ -366,6 +426,7 @@ const BuySellForm = props => { has_character_counter initial_character_count={buy_sell_store.payment_info.length} max_characters={300} + disabled={should_disable_field} /> )}
@@ -384,6 +445,7 @@ const BuySellForm = props => { has_character_counter initial_character_count={buy_sell_store.contact_info.length} max_characters={300} + disabled={should_disable_field} /> )} @@ -392,10 +454,10 @@ const BuySellForm = props => { )}
- - ); - }} - + ); + }} + + ); }; diff --git a/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx b/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx index 2f6930fc43a5..0b6b7273f6eb 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-modal.jsx @@ -20,6 +20,22 @@ import BuySellFormReceiveAmount from './buy-sell-form-receive-amount.jsx'; import NicknameForm from '../nickname-form'; import 'Components/buy-sell/buy-sell-modal.scss'; import AddPaymentMethodForm from '../my-profile/payment-methods/add-payment-method/add-payment-method-form.jsx'; +import { api_error_codes } from 'Constants/api-error-codes.js'; + +const LowBalanceMessage = () => ( +
+ + + + } + is_danger + /> +
+); const BuySellModalFooter = ({ onCancel, is_submit_disabled, onSubmit }) => { return ( @@ -71,7 +87,7 @@ const generateModalTitle = (formik_ref, my_profile_store, table_type, selected_a }; const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldShowPopup }) => { - const { buy_sell_store, general_store, my_profile_store, order_store } = useStores(); + const { buy_sell_store, floating_rate_store, general_store, my_profile_store, order_store } = useStores(); const submitForm = React.useRef(() => {}); const [error_message, setErrorMessage] = useSafeState(null); const [is_submit_disabled, setIsSubmitDisabled] = useSafeState(true); @@ -82,33 +98,27 @@ const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldSho receive_amount={buy_sell_store.receive_amount} /> ); + const [is_account_balance_low, setIsAccountBalanceLow] = React.useState(false); const formik_ref = React.useRef(); - const BuySellFormError = () => { - return buy_sell_store.form_error_code === 'OrderCreateFailClientBalance' ? ( + const BuySellFormError = () => ( +
- + {buy_sell_store.form_error_code === api_error_codes.INSUFFICIENT_BALANCE ? ( + + ) : ( + error_message + )} } is_danger /> - ) : ( - - {error_message} - - } - is_danger - /> - ); - }; +
+ ); const onCancel = () => { if (my_profile_store.should_show_add_payment_method_form) { @@ -120,6 +130,8 @@ const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldSho } else { setShouldShowPopup(false); } + floating_rate_store.setIsMarketRateChanged(false); + buy_sell_store.setShowRateChangePopup(false); }; const onConfirmClick = order_info => { @@ -131,6 +143,11 @@ const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldSho const setSubmitForm = submitFormFn => (submitForm.current = submitFormFn); React.useEffect(() => { + const balance_check = + parseFloat(general_store.balance) === 0 || + parseFloat(general_store.balance) < buy_sell_store.advert?.min_order_amount_limit; + + setIsAccountBalanceLow(balance_check); if (!should_show_popup) { setErrorMessage(null); } @@ -158,7 +175,6 @@ const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldSho renderPageFooterChildren={() => !my_profile_store.should_show_add_payment_method_form && ( - {error_message && } + {table_type === buy_sell.SELL && is_account_balance_low && } + {!!error_message && } {my_profile_store.should_show_add_payment_method_form ? ( ) : ( @@ -188,48 +205,52 @@ const BuySellModal = ({ table_type, selected_ad, should_show_popup, setShouldSho ); } + if (should_show_popup) { + return ( + + {/* Parent height - Modal.Header height - Modal.Footer height */} + + + {table_type === buy_sell.SELL && is_account_balance_low && } + {!!error_message && } + {my_profile_store.should_show_add_payment_method_form ? ( + + ) : ( +
+ )} + + + {!my_profile_store.should_show_add_payment_method_form && ( + + {my_profile_store.should_show_add_payment_method_form ? null : ( + + )} + + )} + + ); + } - return ( - - {/* Parent height - Modal.Header height - Modal.Footer height */} - - - {error_message && } - {my_profile_store.should_show_add_payment_method_form ? ( - - ) : ( - - )} - - - {!my_profile_store.should_show_add_payment_method_form && ( - - {my_profile_store.should_show_add_payment_method_form ? null : ( - - )} - - )} - - ); + return null; }; BuySellModal.propTypes = { diff --git a/packages/p2p/src/components/buy-sell/buy-sell-modal.scss b/packages/p2p/src/components/buy-sell/buy-sell-modal.scss index 4be39f2fd908..f2717c4042bc 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-modal.scss +++ b/packages/p2p/src/components/buy-sell/buy-sell-modal.scss @@ -2,10 +2,9 @@ opacity: 1 !important; &-body { - padding: 1.6rem; position: relative; - display: flex; - flex-direction: column; + display: block; + padding-top: 2.4rem; } &-danger { @@ -19,6 +18,15 @@ margin-right: 0.7rem; flex: 1; + &--disable { + opacity: 0.32; + } + + &--textarea { + border-top: 1px solid var(--general-section-5); + padding: 1.8rem 2.4rem 0; + } + @include mobile { margin: 0.8rem 0; } @@ -27,6 +35,8 @@ $gap: 2.4rem; display: flex; margin: -#{$gap} 0 1.6rem -#{$gap}; + padding: 0 2.4rem; + word-break: break-word; & > * { margin: $gap 0 0 $gap; @@ -47,22 +57,52 @@ } } + &--input { + display: flex; + gap: 2rem; + flex-direction: column; + padding: 0 2.4rem 1.4rem; + .dc-input__wrapper { + margin-bottom: unset; + } + + &-field { + @include desktop() { + display: flex; + gap: 2rem; + } + } + } + + &-hintbox { + margin: -0.5rem 0 2.4rem; + padding: 0 2.4rem; + + .dc-hint-box { + &__icon { + align-self: flex-start; + } + } + } + &-icon { margin-right: 0.8rem; } &-line { - border-top: 1px solid var(--general-section-1); + border-top: 1px solid var(--general-section-5); margin: 1.6rem 0; width: 100%; } &-payment-method { + padding: 0 2.4rem; &--container { align-self: center; display: flex; flex-direction: column; margin-bottom: 1.6rem; + padding: 0 2.4rem; } &--icon { @@ -113,6 +153,18 @@ } } } + + &--disable { + opacity: 0.32; + } + } + + &--layout { + padding: unset; + } + + &--error-message { + padding: 0 2.4rem; } } @@ -121,3 +173,13 @@ margin-bottom: unset; } } +.dc-modal__container_buy-sell__modal { + .dc-modal-header--buy-sell__modal { + border-bottom: 2px solid var(--general-section-5); + } + + .dc-modal-footer--separator { + margin-top: 1rem; + border-top: 2px solid var(--general-section-5); + } +} diff --git a/packages/p2p/src/components/buy-sell/buy-sell-row.jsx b/packages/p2p/src/components/buy-sell/buy-sell-row.jsx index 4bbc56180edc..4991d5b085b6 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-row.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-row.jsx @@ -8,11 +8,12 @@ import { buy_sell } from 'Constants/buy-sell'; import { Localize, localize } from 'Components/i18next'; import UserAvatar from 'Components/user/user-avatar'; import { useStores } from 'Stores'; +import { generateEffectiveRate } from 'Utils/format-value.js'; import './buy-sell-row.scss'; import TradeBadge from '../trade-badge'; const BuySellRow = ({ row: advert }) => { - const { buy_sell_store, general_store } = useStores(); + const { buy_sell_store, floating_rate_store, general_store } = useStores(); if (advert.id === 'WATCH_THIS_SPACE') { // This allows for the sliding animation on the Buy/Sell toggle as it pushes @@ -41,11 +42,20 @@ const BuySellRow = ({ row: advert }) => { min_order_amount_limit_display, payment_method_names, price_display, + rate_type, + rate, } = advert; const is_my_advert = advert.advertiser_details.id === general_store.advertiser_id; const is_buy_advert = counterparty_type === buy_sell.BUY; const { name: advertiser_name } = advert.advertiser_details; + const { display_effective_rate } = generateEffectiveRate({ + price: price_display, + rate_type, + rate, + local_currency, + exchange_rate: floating_rate_store.exchange_rate, + }); if (isMobile()) { return ( @@ -92,7 +102,7 @@ const BuySellRow = ({ row: advert }) => { /> - {price_display} {local_currency} + {display_effective_rate} {local_currency} { - {price_display} {local_currency} + {display_effective_rate} {local_currency} diff --git a/packages/p2p/src/components/buy-sell/buy-sell-table.jsx b/packages/p2p/src/components/buy-sell/buy-sell-table.jsx index bbde03199e28..71354780233f 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell-table.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell-table.jsx @@ -34,14 +34,7 @@ const BuySellTable = ({ onScroll }) => { my_profile_store.setIsCancelAddPaymentMethodModalOpen(false); reaction( () => buy_sell_store.is_buy, - () => { - buy_sell_store.setItems([]); - buy_sell_store.setIsLoading(true); - buy_sell_store.loadMoreItems({ startIndex: 0 }); - if (!buy_sell_store.is_buy) { - my_profile_store.getAdvertiserPaymentMethods(); - } - }, + () => buy_sell_store.fetchAdvertiserAdverts(), { fireImmediately: true } ); }, diff --git a/packages/p2p/src/components/buy-sell/buy-sell.jsx b/packages/p2p/src/components/buy-sell/buy-sell.jsx index 217e1bf65d42..1ecfc9dc5857 100644 --- a/packages/p2p/src/components/buy-sell/buy-sell.jsx +++ b/packages/p2p/src/components/buy-sell/buy-sell.jsx @@ -6,6 +6,7 @@ import { localize } from 'Components/i18next'; import AdvertiserPage from 'Components/advertiser-page/advertiser-page.jsx'; import PageReturn from 'Components/page-return/page-return.jsx'; import Verification from 'Components/verification/verification.jsx'; +import RateChangeModal from 'Components/buy-sell/rate-change-modal.jsx'; import { buy_sell } from 'Constants/buy-sell'; import { useStores } from 'Stores'; import BuySellHeader from './buy-sell-header.jsx'; @@ -79,6 +80,7 @@ const BuySell = () => { setShouldShowPopup={buy_sell_store.setShouldShowPopup} table_type={buy_sell_store.table_type} /> +
); }; diff --git a/packages/p2p/src/components/buy-sell/rate-change-modal.jsx b/packages/p2p/src/components/buy-sell/rate-change-modal.jsx new file mode 100644 index 000000000000..778fe8c92179 --- /dev/null +++ b/packages/p2p/src/components/buy-sell/rate-change-modal.jsx @@ -0,0 +1,52 @@ +import { observer } from 'mobx-react-lite'; +import React from 'react'; +import { Button, Modal, Text } from '@deriv/components'; +import { isMobile } from '@deriv/shared'; +import { localize, Localize } from 'Components/i18next'; +import { useStores } from 'Stores'; +import './rate-change-modal.scss'; + +const RateChangeModal = ({ onMount }) => { + const { buy_sell_store, floating_rate_store, general_store } = useStores(); + const local_currency = general_store.client?.local_currency_config?.currency; + const is_mobile = isMobile(); + + const closeModal = () => { + floating_rate_store.setIsMarketRateChanged(false); + buy_sell_store.setShowRateChangePopup(false); + onMount(false); + }; + + if (!is_mobile && floating_rate_store.is_market_rate_changed) { + onMount(false); + } + + return ( + + + + + + + + - -
- - - -
- ); - }} - +
+ + +
+ + + +
+ ); + }} + +
+ )} + { - const { general_store } = useStores(); + const { floating_rate_store, general_store, my_ads_store } = useStores(); const { currency, local_currency_config } = general_store.client; - const display_offer_amount = offer_amount ? formatMoney(currency, offer_amount, true) : ''; - const display_price_rate = price_rate ? formatMoney(local_currency_config.currency, price_rate, true) : ''; - const display_total = - offer_amount && price_rate ? formatMoney(local_currency_config.currency, offer_amount * price_rate, true) : ''; + const market_feed = my_ads_store.required_ad_type === ad_type.FLOAT ? floating_rate_store.exchange_rate : null; + + let display_price_rate = ''; + let display_total = ''; + + if (price_rate) { + display_price_rate = market_feed ? roundOffDecimal(percentOf(market_feed, price_rate), 6) : price_rate; + } + + if (offer_amount && price_rate) { + display_total = market_feed + ? formatMoney(local_currency_config.currency, offer_amount * display_price_rate, true) + : formatMoney(local_currency_config.currency, offer_amount * price_rate, true); + } if (offer_amount) { - const components = []; + const components = [ + , + , + ]; const values = { target_amount: display_offer_amount, target_currency: currency }; if (price_rate) { Object.assign(values, { local_amount: display_total, local_currency: local_currency_config.currency, - price_rate: display_price_rate, + price_rate: removeTrailingZeros( + formatMoney(local_currency_config.currency, display_price_rate, true, 6) + ), }); if (type === buy_sell.BUY) { return ( @@ -39,7 +56,7 @@ const EditAdSummary = ({ offer_amount, price_rate, type }) => { return ( @@ -75,6 +92,7 @@ const EditAdSummary = ({ offer_amount, price_rate, type }) => { EditAdSummary.propTypes = { offer_amount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), price_rate: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + market_feed: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), type: PropTypes.string, }; diff --git a/packages/p2p/src/components/my-ads/my-ads-floating-rate-switch-modal.jsx b/packages/p2p/src/components/my-ads/my-ads-floating-rate-switch-modal.jsx new file mode 100644 index 000000000000..ad02e588963b --- /dev/null +++ b/packages/p2p/src/components/my-ads/my-ads-floating-rate-switch-modal.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Localize } from 'Components/i18next'; +import { Button, Modal } from '@deriv/components'; +import { observer } from 'mobx-react-lite'; +import { ad_type } from 'Constants/floating-rate'; +import { useStores } from 'Stores'; + +const MyAdsFloatingRateSwitchModal = () => { + const { floating_rate_store, my_ads_store } = useStores(); + + return ( + + my_ads_store.toggleMyAdsRateSwitchModal(my_ads_store.selected_ad_type)} + small + className='switch-ads' + > + + {floating_rate_store.rate_type === ad_type.FLOAT ? ( + + ) : ( + + )} + + + + + + + + + + ); +}; + +export default observer(MyAdsFloatingRateSwitchModal); diff --git a/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx b/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx index bce7ed0f56b0..713b504f10aa 100644 --- a/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx +++ b/packages/p2p/src/components/my-ads/my-ads-row-renderer.jsx @@ -6,11 +6,14 @@ import { isMobile, formatMoney } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; import { Localize, localize } from 'Components/i18next'; import { buy_sell } from 'Constants/buy-sell'; +import { ad_type } from 'Constants/floating-rate'; import AdStatus from 'Components/my-ads/ad-status.jsx'; import { useStores } from 'Stores'; +import { generateEffectiveRate } from 'Utils/format-value.js'; +import AdType from './ad-type.jsx'; const MyAdsRowRenderer = observer(({ row: advert, setAdvert }) => { - const { general_store, my_ads_store, my_profile_store } = useStores(); + const { floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores(); const { account_currency, @@ -23,6 +26,8 @@ const MyAdsRowRenderer = observer(({ row: advert, setAdvert }) => { min_order_amount_display, payment_method_names, price_display, + rate_display, + rate_type, remaining_amount, remaining_amount_display, type, @@ -31,283 +36,290 @@ const MyAdsRowRenderer = observer(({ row: advert, setAdvert }) => { // Use separate is_advert_active state to ensure value is updated const [is_advert_active, setIsAdvertActive] = React.useState(is_active); const [is_popover_actions_visible, setIsPopoverActionsVisible] = React.useState(false); - const amount_dealt = amount - remaining_amount; + const enable_action_point = floating_rate_store.change_ad_alert && floating_rate_store.rate_type !== rate_type; const is_buy_advert = type === buy_sell.BUY; - const onClickAddPaymentMethod = () => { - if (!general_store.is_barred) { - setAdvert(advert); - my_ads_store.showQuickAddModal(advert); + const { display_effective_rate } = generateEffectiveRate({ + price: price_display, + rate_type, + rate: rate_display, + local_currency, + exchange_rate: floating_rate_store.exchange_rate, + }); + + const is_activate_ad_disabled = floating_rate_store.reached_target_date && enable_action_point; + + const onClickActivateDeactivate = () => { + if (!is_activate_ad_disabled) { + my_ads_store.onClickActivateDeactivate(id, is_advert_active, setIsAdvertActive); } }; - const onClickActivateDeactivate = () => - my_ads_store.onClickActivateDeactivate(id, is_advert_active, setIsAdvertActive); - const onClickDelete = () => my_ads_store.onClickDelete(id); - const onClickEdit = () => my_ads_store.onClickEdit(id); + const onClickDelete = () => !general_store.is_barred && my_ads_store.onClickDelete(id); + const onClickEdit = () => !general_store.is_barred && my_ads_store.onClickEdit(id, rate_type); + const onClickSwitchAd = () => !general_store.is_barred && my_ads_store.setIsSwitchModalOpen(true, id); const onMouseEnter = () => setIsPopoverActionsVisible(true); const onMouseLeave = () => setIsPopoverActionsVisible(false); + const handleOnEdit = () => + enable_action_point && floating_rate_store.rate_type !== rate_type ? onClickSwitchAd() : onClickEdit(); + React.useEffect(() => { my_profile_store.getAdvertiserPaymentMethods(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (isMobile()) { return ( - <> - -
- -
- {is_advert_active ? ( -
- -
- ) : ( -
- -
+ +
+ +
+
- -
- - } - right_hidden_component_width='18rem' - visible_component={ - - - - -
- - {is_buy_advert ? ( - - ) : ( - - )} - - -
-
- - {`${formatMoney(account_currency, amount_dealt, true)}`} {account_currency}  - {is_buy_advert ? localize('Bought') : localize('Sold')} - - - {amount_display} {account_currency} - -
- -
- - - - - - -
-
- - {min_order_amount_display} - {max_order_amount_display} {account_currency} - - - {price_display} {local_currency} - -
-
- {payment_method_names ? ( - payment_method_names.map((payment_method, key) => { - return ( -
- - {payment_method} - -
- ); - }) - ) : ( -
- - - - +
+ +
+ +
+ + } + right_hidden_component_width='18rem' + visible_component={ + + + + +
+ + + + {enable_action_point ? ( +
+
+
- )} -
- - } - /> - - ); - } - return ( - <> -
- - - {is_buy_advert ? ( - - ) : ( - - )} - - - {min_order_amount_display}-{max_order_amount_display} {account_currency} - - - {price_display} {local_currency} - - + +
+ ) : ( + + )} +
+
+ + {`${formatMoney(account_currency, amount_dealt, true)}`} {account_currency}  + {is_buy_advert ? localize('Bought') : localize('Sold')} + + + {amount_display} {account_currency} + +
-
- {remaining_amount_display}/{amount_display} {account_currency} +
+ + + + + +
- - -
+
+ + {min_order_amount_display} - {max_order_amount_display} {account_currency} + + +
+ {display_effective_rate} {local_currency} + {rate_type === ad_type.FLOAT && } +
+
+
+
{payment_method_names ? ( payment_method_names.map((payment_method, key) => { return (
- + {payment_method}
); }) ) : ( -
- - +
{ + setAdvert(advert); + my_ads_store.showQuickAddModal(advert); + }} + > + +
)}
- - -
+ + } + /> + ); + } + + return ( +
+ + + + + + {min_order_amount_display}-{max_order_amount_display} {account_currency} + + +
+ {display_effective_rate} {local_currency} + {rate_type === ad_type.FLOAT && } +
+
+ + +
+ {remaining_amount_display}/{amount_display} {account_currency} +
+
+ +
+ {payment_method_names ? ( + payment_method_names.map((payment_method, key) => { + return ( +
+ + {payment_method} + +
+ ); + }) + ) : ( +
{ + setAdvert(advert); + my_ads_store.showQuickAddModal(advert); + }} + > + + + + +
+ )} +
+
+ + {enable_action_point ? ( +
+
-
- {is_popover_actions_visible && ( -
- {is_advert_active ? ( -
- - - -
- ) : ( -
- - - -
- )} -
- - - -
-
- - - -
+ ) : ( +
+
)} - -
- + + {is_popover_actions_visible && ( +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ )} +
+
); }); diff --git a/packages/p2p/src/components/my-ads/my-ads-table.jsx b/packages/p2p/src/components/my-ads/my-ads-table.jsx index 7d41b1621f7b..26e236243301 100644 --- a/packages/p2p/src/components/my-ads/my-ads-table.jsx +++ b/packages/p2p/src/components/my-ads/my-ads-table.jsx @@ -7,9 +7,11 @@ import { localize, Localize } from 'Components/i18next'; import Empty from 'Components/empty/empty.jsx'; import ToggleAds from 'Components/my-ads/toggle-ads.jsx'; import { TableError } from 'Components/table/table-error.jsx'; +import { ad_type } from 'Constants/floating-rate'; import { useStores } from 'Stores'; import { generateErrorDialogTitle } from 'Utils/adverts.js'; import MyAdsDeleteModal from './my-ads-delete-modal.jsx'; +import MyAdsFloatingRateSwitchModal from './my-ads-floating-rate-switch-modal.jsx'; import MyAdsRowRenderer from './my-ads-row-renderer.jsx'; import QuickAddModal from './quick-add-modal.jsx'; import AdExceedsDailyLimitModal from './ad-exceeds-daily-limit-modal.jsx'; @@ -24,18 +26,55 @@ const getHeaders = offered_currency => [ { text: '' }, // empty header for delete and archive icons ]; -const MyAdsTable = () => { - const { general_store, my_ads_store } = useStores(); +const AdSwitchHintBox = () => { + const { floating_rate_store, general_store } = useStores(); + + if (floating_rate_store.rate_type === ad_type.FLOAT) { + return floating_rate_store.reached_target_date ? ( + + ) : ( + + ); + } + + return floating_rate_store.reached_target_date ? ( + + ) : ( + + ); +}; +const MyAdsTable = () => { + const { floating_rate_store, general_store, my_ads_store } = useStores(); const [selected_advert, setSelectedAdvert] = React.useState(undefined); React.useEffect(() => { my_ads_store.setAdverts([]); my_ads_store.setSelectedAdId(''); my_ads_store.loadMoreAds({ startIndex: 0 }, true); + general_store.setP2PConfig(); // eslint-disable-next-line react-hooks/exhaustive-deps - return () => my_ads_store.setApiErrorCode(null); + return () => { + my_ads_store.setApiErrorCode(null); + floating_rate_store.setChangeAdAlert(false); + }; }, []); if (my_ads_store.is_table_loading) { @@ -50,17 +89,18 @@ const MyAdsTable = () => { return ( {selected_advert && } - {my_ads_store.has_missing_payment_methods && ( - - - - } - is_warn - /> + {floating_rate_store.change_ad_alert && ( +
+ + + + } + is_warn + /> +
)}
@@ -115,6 +155,7 @@ const MyAdsTable = () => {
)} + { const { my_profile_store } = useStores(); const method = !is_add && payment_method.display_name.replace(/\s|-/gm, ''); @@ -44,7 +45,7 @@ const PaymentMethodCard = ({ size={32} /> - + {label || add_payment_method}
@@ -133,6 +134,7 @@ PaymentMethodCard.propTypes = { payment_method: PropTypes.object, small: PropTypes.bool, style: PropTypes.object, + disabled: PropTypes.bool, }; export default PaymentMethodCard; diff --git a/packages/p2p/src/components/order-details/__test__/order-details-confirm-modal.spec.js b/packages/p2p/src/components/order-details/__test__/order-details-confirm-modal.spec.js index 573c9881a310..0444e54e84a9 100644 --- a/packages/p2p/src/components/order-details/__test__/order-details-confirm-modal.spec.js +++ b/packages/p2p/src/components/order-details/__test__/order-details-confirm-modal.spec.js @@ -42,7 +42,7 @@ describe('', () => { 'Please confirm only after checking your bank or e-wallet account to make sure you have received payment.' ) ).toBeInTheDocument(); - expect(screen.getByText('I have received 20.00 AED')).toBeInTheDocument(); + expect(screen.getByText("I've received 20.00 AED")).toBeInTheDocument(); expect(screen.getByText('Cancel')).toBeInTheDocument(); }); @@ -51,9 +51,9 @@ describe('', () => { ); - expect(screen.getByText('Confirm payment?')).toBeInTheDocument(); - expect(screen.getByText("Please make sure that you've paid 20.00 AED to P2P.")).toBeInTheDocument(); - expect(screen.getByText('I have paid 20.00 AED')).toBeInTheDocument(); + expect(screen.getByText('Payment confirmation')).toBeInTheDocument(); + expect(screen.getByText('Have you paid 20.00 AED to P2P?')).toBeInTheDocument(); + expect(screen.getByText("Yes, I've paid")).toBeInTheDocument(); expect(screen.getByText("I haven't paid yet")).toBeInTheDocument(); }); diff --git a/packages/p2p/src/components/order-details/__test__/order-details.spec.js b/packages/p2p/src/components/order-details/__test__/order-details.spec.js index e2dbe17dcf93..fa2945933499 100644 --- a/packages/p2p/src/components/order-details/__test__/order-details.spec.js +++ b/packages/p2p/src/components/order-details/__test__/order-details.spec.js @@ -73,6 +73,8 @@ jest.mock('Components/order-details/order-info-block.jsx', () => jest.fn(() => < jest.mock('Components/orders/chat/chat.jsx', () => jest.fn(() =>
Chat section
)); +jest.mock('Components/p2p-accordion/p2p-accordion.jsx', () => jest.fn(() =>
Payment methods listed
)); + describe('', () => { it('should render component with loss of funds warning banner', () => { useStores.mockReturnValue({ @@ -83,14 +85,10 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect( - screen.getByText( - 'To avoid loss of funds, please do not use cash transactions. We recommend using e-wallets or bank transfers.' - ) + screen.getByText("Don't risk your funds with cash transactions. Use bank transfers or e-wallets instead.") ).toBeInTheDocument(); }); - it('should render success message when highlight success is true', () => { useStores.mockReturnValue({ order_store: { @@ -100,10 +98,8 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect(screen.getByText('Result str')).toBeInTheDocument(); }); - it('should display footer info when show_order_footer is set', () => { useStores.mockReturnValue({ order_store: { @@ -113,10 +109,8 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect(screen.getByText('Order details footer')).toBeInTheDocument(); }); - it('should display formatted currency when the order is pending', () => { useStores.mockReturnValue({ order_store: { @@ -126,10 +120,8 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect(screen.getByText('40.00 AED')).toBeInTheDocument(); }); - it('should display payment details when Order is active', () => { useStores.mockReturnValue({ order_store: { @@ -140,10 +132,8 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect(screen.getByText('Payment details')).toBeInTheDocument(); }); - it('should render Chat component if show_chaton_orders is enabled', () => { useStores.mockReturnValue({ order_store: { @@ -152,10 +142,8 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store, should_show_chat_on_orders: true }, }); render(); - expect(screen.getByText('Chat section')).toBeInTheDocument(); }); - it('should display Buy section when is_buy_order flag is enabled', () => { useStores.mockReturnValue({ order_store: { @@ -165,10 +153,8 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect(screen.getByText('Buy USD order')).toBeInTheDocument(); }); - it('should display Buy section when is_sell_order as well as is_my_ad flag is enabled', () => { useStores.mockReturnValue({ order_store: { @@ -178,7 +164,6 @@ describe('', () => { sendbird_store: { ...mock_sendbird_store }, }); render(); - expect(screen.getByText('Buy USD order')).toBeInTheDocument(); }); }); diff --git a/packages/p2p/src/components/order-details/order-details-confirm-modal.jsx b/packages/p2p/src/components/order-details/order-details-confirm-modal.jsx index 2a1b59879b80..4839e923d34b 100644 --- a/packages/p2p/src/components/order-details/order-details-confirm-modal.jsx +++ b/packages/p2p/src/components/order-details/order-details-confirm-modal.jsx @@ -3,9 +3,10 @@ import React from 'react'; import { Button, Checkbox, Loading, Modal, Text } from '@deriv/components'; import { useIsMounted } from '@deriv/shared'; import { Localize } from 'Components/i18next'; -import { requestWS } from 'Utils/websocket'; import FormError from 'Components/form/error.jsx'; import 'Components/order-details/order-details-confirm-modal.scss'; +import { requestWS } from 'Utils/websocket'; +import { setDecimalPlaces, roundOffDecimal } from 'Utils/format-value.js'; const OrderDetailsConfirmModal = ({ order_information, @@ -45,9 +46,11 @@ const OrderDetailsConfirmModal = ({ .finally(() => setIsProcessRequest(false)); }; + const rounded_rate = roundOffDecimal(rate, setDecimalPlaces(rate, 6)); + const getConfirmButtonText = () => { if (is_buy_order_for_user) { - return ; + return ; } else if (is_process_request) { return ; } @@ -70,11 +73,11 @@ const OrderDetailsConfirmModal = ({ has_close_icon renderTitle={() => ( - {is_buy_order_for_user ? ( - - ) : ( - - )} + )} width='440px' @@ -83,9 +86,9 @@ const OrderDetailsConfirmModal = ({ {is_buy_order_for_user ? ( + ) : ( diff --git a/packages/p2p/src/components/order-details/order-details.jsx b/packages/p2p/src/components/order-details/order-details.jsx index 2c8e4f4da59a..37ffedca8050 100644 --- a/packages/p2p/src/components/order-details/order-details.jsx +++ b/packages/p2p/src/components/order-details/order-details.jsx @@ -1,8 +1,8 @@ import classNames from 'classnames'; import React from 'react'; import PropTypes from 'prop-types'; -import { Accordion, HintBox, Text, ThemedScrollbars } from '@deriv/components'; -import { getFormattedText, isDesktop } from '@deriv/shared'; +import { Button, HintBox, Text, ThemedScrollbars } from '@deriv/components'; +import { formatMoney, isDesktop } from '@deriv/shared'; import { observer } from 'mobx-react-lite'; import { Localize, localize } from 'Components/i18next'; import Chat from 'Components/orders/chat/chat.jsx'; @@ -10,13 +10,16 @@ import OrderDetailsFooter from 'Components/order-details/order-details-footer.js import OrderDetailsTimer from 'Components/order-details/order-details-timer.jsx'; import OrderInfoBlock from 'Components/order-details/order-info-block.jsx'; import OrderDetailsWrapper from 'Components/order-details/order-details-wrapper.jsx'; +import P2PAccordion from 'Components/p2p-accordion/p2p-accordion.jsx'; import { useStores } from 'Stores'; import PaymentMethodAccordionHeader from './payment-method-accordion-header.jsx'; import PaymentMethodAccordionContent from './payment-method-accordion-content.jsx'; import MyProfileSeparatorContainer from '../my-profile/my-profile-separator-container'; +import { setDecimalPlaces, removeTrailingZeros, roundOffDecimal } from 'Utils/format-value.js'; import 'Components/order-details/order-details.scss'; const OrderDetails = observer(({ onPageReturn }) => { + const [should_expand_all, setShouldExpandAll] = React.useState(false); const { order_store, sendbird_store } = useStores(); const { account_currency, @@ -70,10 +73,15 @@ const OrderDetails = observer(({ onPageReturn }) => { (is_buy_order && !is_my_ad) || (is_sell_order && is_my_ad) ? localize('Buy {{offered_currency}} order', { offered_currency: account_currency }) : localize('Sell {{offered_currency}} order', { offered_currency: account_currency }); + if (sendbird_store.should_show_chat_on_orders) { return ; } + const display_payment_amount = removeTrailingZeros( + formatMoney(local_currency, amount_display * roundOffDecimal(rate, setDecimalPlaces(rate, 6)), true) + ); + return ( {should_show_lost_funds_banner && ( @@ -82,7 +90,7 @@ const OrderDetails = observer(({ onPageReturn }) => { icon='IcAlertWarning' message={ - + } is_warn @@ -111,7 +119,7 @@ const OrderDetails = observer(({ onPageReturn }) => { )} {!has_timer_expired && (is_pending_order || is_buyer_confirmed_order) && (
- {getFormattedText(amount_display * rate, local_currency)} + {display_payment_amount} {local_currency}
)}
@@ -127,11 +135,7 @@ const OrderDetails = observer(({ onPageReturn }) => {
- {other_user_details.name} - - } + value={{other_user_details.name}} />
@@ -145,11 +149,11 @@ const OrderDetails = observer(({ onPageReturn }) => {
@@ -160,56 +164,78 @@ const OrderDetails = observer(({ onPageReturn }) => {
- - {is_active_order && - (order_store?.has_order_payment_method_details ? ( -
- - {labels.payment_details} - - ({ - header: , - content: , - }))} + {is_active_order && ( + + + {order_store?.has_order_payment_method_details ? ( +
+
+ + {labels.payment_details} + + +
+ ({ + header: ( + + ), + content: ( + + ), + }))} + is_expand_all={should_expand_all} + onChange={setShouldExpandAll} + /> +
+ ) : ( + -
- ) : ( + )} + + )} + {is_active_order && ( + + - {labels.payment_details} - - } - value={payment_info || '-'} + label={labels.contact_details} + size='xs' + weight='bold' + value={contact_info || '-'} + /> + + - ))} - - - {labels.contact_details} - - } - value={contact_info || '-'} - /> - - - {labels.instructions} - - } - value={advert_details.description.trim() || '-'} - /> - {should_show_order_footer && isDesktop() && ( - + {should_show_order_footer && isDesktop() && ( + + )} + )} {should_show_order_footer && isDesktop() && ( diff --git a/packages/p2p/src/components/order-details/order-details.scss b/packages/p2p/src/components/order-details/order-details.scss index 99f264f75dde..e397cc58f2a9 100644 --- a/packages/p2p/src/components/order-details/order-details.scss +++ b/packages/p2p/src/components/order-details/order-details.scss @@ -54,6 +54,11 @@ $card-width: 456px; width: 100%; } + &__title { + display: flex; + justify-content: space-between; + } + &--padding { padding: 1rem 2.4rem; @@ -99,6 +104,7 @@ $card-width: 456px; color: var(--text-general); position: relative; border-bottom: 1px solid var(--general-section-1); + align-items: center; @include tablet-up { padding: 1.6rem 2.4rem; diff --git a/packages/p2p/src/components/order-details/order-info-block.jsx b/packages/p2p/src/components/order-details/order-info-block.jsx index 60fa64b30dba..1634dcd87481 100644 --- a/packages/p2p/src/components/order-details/order-info-block.jsx +++ b/packages/p2p/src/components/order-details/order-info-block.jsx @@ -3,9 +3,9 @@ import PropTypes from 'prop-types'; import { Text } from '@deriv/components'; import classNames from 'classnames'; -const OrderInfoBlock = ({ className, label, value }) => ( +const OrderInfoBlock = ({ className, label, value, size = 'xxs', weight = 'normal' }) => (
- + {label}
{value}
diff --git a/packages/p2p/src/components/orders/order-table/order-table-row.jsx b/packages/p2p/src/components/orders/order-table/order-table-row.jsx index 71da512e8b89..e0cba5bfa01e 100644 --- a/packages/p2p/src/components/orders/order-table/order-table-row.jsx +++ b/packages/p2p/src/components/orders/order-table/order-table-row.jsx @@ -103,7 +103,11 @@ const OrderRow = ({ style, row: order }) => { 'orders__table-grid--active': general_store.is_active_tab, })} > -
{ })} > {status_string} -
+
{is_timer_visible && ( @@ -162,7 +166,10 @@ const OrderRow = ({ style, row: order }) => { {id} {other_user_details.name} -
{ })} > {status_string} -
+
{is_buy_order_type_for_user ? transaction_amount : offer_amount} {is_buy_order_type_for_user ? offer_amount : transaction_amount} diff --git a/packages/p2p/src/components/orders/orders.scss b/packages/p2p/src/components/orders/orders.scss index 2b4e1bf71d92..82ad6e996ee8 100644 --- a/packages/p2p/src/components/orders/orders.scss +++ b/packages/p2p/src/components/orders/orders.scss @@ -152,9 +152,6 @@ &-status { width: auto; padding: 0.2rem 1.6rem; - font-weight: bold; - text-align: center; - font-size: var(--text-size-xs); border-radius: 1.6rem; } } @@ -196,14 +193,14 @@ } &-grid { // Sizes are different for inactive due to timeout being full time string. - grid-template-columns: 1.5fr 1.5fr 2fr 2.5fr 1.5fr 1.5fr 2fr; + grid-template-columns: 1fr 1.5fr 2fr 3fr 1.5fr 1.5fr 2fr; &--active { - grid-template-columns: 1.5fr 1.5fr 2fr 2.5fr 1.5fr 1.5fr 1.5fr; + grid-template-columns: 1fr 1.5fr 2fr 3fr 1.5fr 1.5fr 1.5fr; + margin-right: 0.5rem; } } &-status { - font-weight: bold; border-radius: 16px; padding: 0.2rem 1.6rem; white-space: nowrap; diff --git a/packages/p2p/src/components/orders/popup.jsx b/packages/p2p/src/components/orders/popup.jsx index ef52425800d9..ae59b264800a 100644 --- a/packages/p2p/src/components/orders/popup.jsx +++ b/packages/p2p/src/components/orders/popup.jsx @@ -56,7 +56,7 @@ const FormWithConfirmation = observer( setFieldValue('need_confirmation', !values.need_confirmation) } defaultChecked={values.need_confirmation} - label={localize('I have received {{amount}} {{currency}}.', { + label={localize("I've received {{amount}} {{currency}}.", { amount: order_information.amount * order_information.rate, currency: order_information.local_currency, })} diff --git a/packages/p2p/src/components/p2p-accordion/p2p-accordion.jsx b/packages/p2p/src/components/p2p-accordion/p2p-accordion.jsx new file mode 100644 index 000000000000..61c4db177025 --- /dev/null +++ b/packages/p2p/src/components/p2p-accordion/p2p-accordion.jsx @@ -0,0 +1,97 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { Icon } from '@deriv/components'; + +const usePrevious = value => { + const ref = React.useRef(); + React.useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +}; + +// This component is an enhancement over Accordion component, created to handle functionalities such as - +// 1. Expand all, collapse all +// 2. Opening one tab must not close the previous opened tab +const P2PAccordion = ({ className, icon_close, icon_open, list, is_expand_all, onChange }) => { + const [open_idx, setOpenIdx] = React.useState({}); + const prev_list = usePrevious(list); + const first_render = React.useRef(true); + + React.useEffect(() => { + if (prev_list !== list) { + const state_ref = [...Array(list.length).keys()].reduce((acc, val) => ({ ...acc, [val]: false }), {}); + setOpenIdx(state_ref); + } + }, [list, prev_list]); + + React.useEffect(() => { + let state_ref; + if (is_expand_all) { + state_ref = [...Array(list.length).keys()].reduce((acc, val) => ({ ...acc, [val]: true }), {}); + } else { + state_ref = [...Array(list.length).keys()].reduce((acc, val) => ({ ...acc, [val]: false }), {}); + } + setOpenIdx(state_ref); + }, [is_expand_all]); + + React.useLayoutEffect(() => { + // Prevent re-render on initial state update + if (first_render.current) { + first_render.current = false; + return; + } + if (is_expand_all) { + const is_all_collapsed = Object.values(open_idx).every(state => !state); + if (is_all_collapsed) { + onChange(false); + } + } else { + const is_all_expanded = Object.values(open_idx).every(state => state); + if (is_all_expanded) { + onChange(true); + } + } + }, [open_idx]); + + const onClick = index => setOpenIdx(prev_state => ({ ...prev_state, [index]: !prev_state[index] })); + + return ( +
+ {list.map((item, idx) => ( +
+
onClick(idx)}> + {item.header} +
+ {open_idx[idx] ? ( + + ) : ( + + )} +
+
+
{item.content}
+
+ ))} +
+ ); +}; + +P2PAccordion.propTypes = { + className: PropTypes.string, + list: PropTypes.arrayOf(PropTypes.object), + is_expand_all: PropTypes.bool, // Expands all Child elements +}; + +export default P2PAccordion; diff --git a/packages/p2p/src/components/page-return/page-return.scss b/packages/p2p/src/components/page-return/page-return.scss index 8ca95bcc383b..cc76c54d5973 100644 --- a/packages/p2p/src/components/page-return/page-return.scss +++ b/packages/p2p/src/components/page-return/page-return.scss @@ -13,5 +13,8 @@ cursor: pointer; border-radius: $BORDER_RADIUS; padding-right: 0.8rem; + @include mobile { + padding-right: 1rem; + } } } diff --git a/packages/p2p/src/constants/api-error-codes.js b/packages/p2p/src/constants/api-error-codes.js index 726585ac5f81..21870c78a30b 100644 --- a/packages/p2p/src/constants/api-error-codes.js +++ b/packages/p2p/src/constants/api-error-codes.js @@ -1,4 +1,6 @@ export const api_error_codes = Object.freeze({ ADVERT_SAME_LIMITS: 'AdvertSameLimits', DUPLICATE_ADVERT: 'DuplicateAdvert', + MARKET_RATE_CHANGE: 'OrderCreateFailRateChanged', + INSUFFICIENT_BALANCE: 'OrderCreateFailClientBalance', }); diff --git a/packages/p2p/src/constants/floating-rate.js b/packages/p2p/src/constants/floating-rate.js new file mode 100644 index 000000000000..afb130582888 --- /dev/null +++ b/packages/p2p/src/constants/floating-rate.js @@ -0,0 +1,4 @@ +export const ad_type = Object.freeze({ + FLOAT: 'float', + FIXED: 'fixed', +}); diff --git a/packages/p2p/src/stores/buy-sell-store.js b/packages/p2p/src/stores/buy-sell-store.js index 1221e6278be0..61c35f57ee9d 100644 --- a/packages/p2p/src/stores/buy-sell-store.js +++ b/packages/p2p/src/stores/buy-sell-store.js @@ -1,10 +1,11 @@ import { action, computed, observable, reaction } from 'mobx'; -import { formatMoney, getDecimalPlaces, getRoundedNumber, isMobile } from '@deriv/shared'; +import { formatMoney, getDecimalPlaces, isMobile } from '@deriv/shared'; import { localize } from 'Components/i18next'; import { buy_sell } from 'Constants/buy-sell'; import { requestWS } from 'Utils/websocket'; import { textValidator, lengthValidator } from 'Utils/validations'; import { countDecimalPlaces } from 'Utils/string'; +import { removeTrailingZeros } from 'Utils/format-value.js'; import BaseStore from 'Stores/base_store'; export default class BuySellStore extends BaseStore { @@ -33,6 +34,7 @@ export default class BuySellStore extends BaseStore { @observable should_use_client_limits = false; @observable show_advertiser_page = false; @observable show_filter_payment_methods = false; + @observable show_rate_change_popup = false; @observable sort_by = 'rate'; @observable submitForm = () => {}; @observable table_type = buy_sell.BUY; @@ -123,6 +125,16 @@ export default class BuySellStore extends BaseStore { return my_profile_store.payment_methods_list_values !== this.selected_payment_method_value; } + @action.bound + fetchAdvertiserAdverts() { + this.setItems([]); + this.setIsLoading(true); + this.loadMoreItems({ startIndex: 0 }); + if (!this.is_buy) { + this.root_store.my_profile_store.getAdvertiserPaymentMethods(); + } + } + @action.bound getAdvertiserInfo() { requestWS({ @@ -160,7 +172,7 @@ export default class BuySellStore extends BaseStore { this.form_props.setErrorMessage(null); - const order = await requestWS({ + const payload = { p2p_order_create: 1, advert_id: this.advert.id, amount: values.amount, @@ -172,12 +184,20 @@ export default class BuySellStore extends BaseStore { contact_info: values.contact_info, } : {}), - }); + }; + if (values.rate !== null) { + payload.rate = values.rate; + } + + const order = await requestWS({ ...payload }); if (order.error) { this.form_props.setErrorMessage(order.error.message); this.setFormErrorCode(order.error.code); } else { + this.form_props.setErrorMessage(null); + this.setShowRateChangePopup(false); + this.root_store.floating_rate_store.setIsMarketRateChanged(false); const response = await requestWS({ p2p_order_info: 1, id: order.p2p_order_create.id }); this.form_props.handleConfirm(response.p2p_order_info); this.form_props.handleClose(); @@ -392,11 +412,8 @@ export default class BuySellStore extends BaseStore { } @action.bound - setInitialReceiveAmount() { - this.receive_amount = getRoundedNumber( - this.advert.min_order_amount_limit * this.advert.price, - this.advert.local_currency - ); + setInitialReceiveAmount(initial_price) { + this.receive_amount = removeTrailingZeros(this.advert.min_order_amount_limit * initial_price); } @action.bound @@ -437,6 +454,9 @@ export default class BuySellStore extends BaseStore { @action.bound setShouldShowPopup(should_show_popup) { this.should_show_popup = should_show_popup; + if (!this.should_show_popup) { + this.fetchAdvertiserAdverts(); + } } @action.bound @@ -494,6 +514,11 @@ export default class BuySellStore extends BaseStore { this.setShowAdvertiserPage(true); } + @action.bound + setShowRateChangePopup(show_rate_change_popup) { + this.show_rate_change_popup = show_rate_change_popup; + } + @action.bound showVerification() { this.setShouldShowVerification(true); @@ -506,6 +531,7 @@ export default class BuySellStore extends BaseStore { v => !!v, v => v >= this.advert.min_order_amount_limit, v => v <= this.advert.max_order_amount_limit, + v => (this.root_store.buy_sell_store.is_buy_advert ? true : v <= this.root_store.general_store.balance), v => countDecimalPlaces(v) <= getDecimalPlaces(this.account_currency), ], }; @@ -530,6 +556,10 @@ export default class BuySellStore extends BaseStore { currency: this.account_currency, value: display_max_amount, }), + localize('Maximum is {{value}} {{currency}}', { + currency: this.account_currency, + value: formatMoney(this.account_currency, this.root_store.general_store.balance, true), + }), localize('Enter a valid amount'), ]; diff --git a/packages/p2p/src/stores/floating-rate-store.js b/packages/p2p/src/stores/floating-rate-store.js new file mode 100644 index 000000000000..3bff6fe38b51 --- /dev/null +++ b/packages/p2p/src/stores/floating-rate-store.js @@ -0,0 +1,105 @@ +import { action, computed, observable } from 'mobx'; +import { ad_type } from 'Constants/floating-rate'; +import BaseStore from 'Stores/base_store'; +import ServerTime from 'Utils/server-time'; +import { roundOffDecimal, removeTrailingZeros } from 'Utils/format-value'; + +export default class FloatingRateStore extends BaseStore { + @observable fixed_rate_adverts_status; + @observable float_rate_adverts_status; + @observable float_rate_offset_limit; + @observable fixed_rate_adverts_end_date; + @observable exchange_rate; + @observable change_ad_alert = false; + @observable is_loading; + @observable api_error_message = ''; + @observable is_market_rate_changed = false; + + previous_exchange_rate = null; + current_exchange_rate = null; + + exchange_rate_subscription = {}; + + @computed + get rate_type() { + if (this.float_rate_adverts_status === 'enabled') { + return ad_type.FLOAT; + } + return ad_type.FIXED; + } + + @computed + get reached_target_date() { + // Ensuring the date is translated to EOD GMT without the time difference + const current_date = new Date(ServerTime.get()) ?? new Date(new Date().getTime()).setUTCHours(23, 59, 59, 999); + const cutoff_date = new Date(new Date(this.fixed_rate_adverts_end_date).getTime()).setUTCHours(23, 59, 59, 999); + return current_date > cutoff_date; + } + + @action.bound + setFixedRateAdvertStatus(fixed_rate_advert_status) { + this.fixed_rate_adverts_status = fixed_rate_advert_status; + } + @action.bound + setFloatingRateAdvertStatus(floating_rate_advert_status) { + this.float_rate_adverts_status = floating_rate_advert_status; + } + @action.bound + setFloatRateOffsetLimit(offset_limit) { + this.float_rate_offset_limit = parseFloat(offset_limit).toFixed(2); + } + @action.bound + setFixedRateAdvertsEndDate(end_date) { + this.fixed_rate_adverts_end_date = end_date; + } + @action.bound + setChangeAdAlert(is_alert_set) { + this.change_ad_alert = is_alert_set; + } + @action.bound + setApiErrorMessage(api_error_message) { + this.api_error_message = api_error_message; + } + @action.bound + setIsLoading(state) { + this.is_loading = state; + } + + @action.bound + setExchangeRate(rate) { + const fetched_rate = parseFloat(rate); + this.exchange_rate = removeTrailingZeros(roundOffDecimal(fetched_rate, 6)); + if (this.previous_exchange_rate === null) { + this.previous_exchange_rate = this.exchange_rate; + this.current_exchange_rate = this.exchange_rate; + } else { + this.previous_exchange_rate = this.current_exchange_rate; + this.current_exchange_rate = this.exchange_rate; + this.setIsMarketRateChanged(true); + } + } + + @action.bound + setIsMarketRateChanged(value) { + if (this.root_store.buy_sell_store.show_rate_change_popup) { + this.is_market_rate_changed = value; + } + } + + @action.bound + fetchExchangeRate(response) { + const { local_currency_config, ws_subscriptions } = this.root_store.general_store.client; + if (response) { + if (response.error) { + this.setApiErrorMessage(response.error.message); + if (ws_subscriptions.exchange_rate_subscription.unsubscribe) { + ws_subscriptions.unsubscribe(); + } + } else { + const { rates } = response.exchange_rates; + this.setExchangeRate(rates[local_currency_config?.currency]); + this.setApiErrorMessage(null); + } + } + } +} diff --git a/packages/p2p/src/stores/general-store.js b/packages/p2p/src/stores/general-store.js index be7fee4c3e73..a86733fbc4ac 100644 --- a/packages/p2p/src/stores/general-store.js +++ b/packages/p2p/src/stores/general-store.js @@ -12,6 +12,7 @@ export default class GeneralStore extends BaseStore { @observable active_index = 0; @observable active_notification_count = 0; @observable advertiser_id = null; + @observable balance; @observable inactive_notification_count = 0; @observable is_advertiser = false; @observable is_blocked = false; @@ -45,7 +46,17 @@ export default class GeneralStore extends BaseStore { @computed get client() { - return this.props?.client || {}; + return { ...this.props?.client } || {}; + } + + @computed + get current_focus() { + return this.props?.current_focus; + } + + @computed + get setCurrentFocus() { + return this.props?.setCurrentFocus; } @computed @@ -230,6 +241,9 @@ export default class GeneralStore extends BaseStore { this.setIsLoading(false); const { sendbird_store } = this.root_store; + + this.setP2PConfig(); + this.ws_subscriptions = { advertiser_subscription: subscribeWS( { @@ -247,6 +261,15 @@ export default class GeneralStore extends BaseStore { }, [this.setP2pOrderList] ), + exchange_rate_subscription: subscribeWS( + { + exchange_rates: 1, + base_currency: this.client.currency, + subscribe: 1, + target_currency: this.client.local_currency_config?.currency, + }, + [this.root_store.floating_rate_store.fetchExchangeRate] + ), }; if (this.ws_subscriptions) { @@ -303,6 +326,11 @@ export default class GeneralStore extends BaseStore { this.active_notification_count = active_notification_count; } + @action.bound + setAccountBalance(value) { + this.balance = value; + } + @action.bound setAdvertiserId(advertiser_id) { this.advertiser_id = advertiser_id; @@ -381,6 +409,24 @@ export default class GeneralStore extends BaseStore { this.order_table_type = order_table_type; } + @action.bound + setP2PConfig() { + const { floating_rate_store } = this.root_store; + requestWS({ website_status: 1 }).then(response => { + if (!!response && response.error) { + floating_rate_store.setApiErrorMessage(response.error.message); + } else { + const { fixed_rate_adverts, float_rate_adverts, float_rate_offset_limit, fixed_rate_adverts_end_date } = + response.website_status.p2p_config; + floating_rate_store.setFixedRateAdvertStatus(fixed_rate_adverts); + floating_rate_store.setFloatingRateAdvertStatus(float_rate_adverts); + floating_rate_store.setFloatRateOffsetLimit(float_rate_offset_limit); + floating_rate_store.setFixedRateAdvertsEndDate(fixed_rate_adverts_end_date || null); + floating_rate_store.setApiErrorMessage(null); + } + }); + } + @action.bound setP2pOrderList(order_response) { if (order_response.error) { diff --git a/packages/p2p/src/stores/index.js b/packages/p2p/src/stores/index.js index 6819386382f9..805c441d10dd 100644 --- a/packages/p2p/src/stores/index.js +++ b/packages/p2p/src/stores/index.js @@ -2,6 +2,7 @@ import React from 'react'; import GeneralStore from './general-store'; import AdvertiserPageStore from './advertiser-page-store'; import BuySellStore from './buy-sell-store'; +import FloatingRateStore from './floating-rate-store'; import MyAdsStore from './my-ads-store'; import MyProfileStore from './my-profile-store'; import OrderStore from './order-store'; @@ -13,11 +14,13 @@ class RootStore { this.general_store = new GeneralStore(this); // Leave at the top! this.advertiser_page_store = new AdvertiserPageStore(this); this.buy_sell_store = new BuySellStore(this); + this.floating_rate_store = new FloatingRateStore(this); this.my_ads_store = new MyAdsStore(this); this.my_profile_store = new MyProfileStore(this); this.order_store = new OrderStore(this); this.order_details_store = new OrderDetailsStore(this); this.sendbird_store = new SendbirdStore(this); + this.floating_rate_store = new FloatingRateStore(this); } } @@ -29,7 +32,6 @@ export const useStores = () => { stores_context = React.createContext({ general_store: root_store.general_store, - advertiser_page_store: root_store.advertiser_page_store, buy_sell_store: root_store.buy_sell_store, my_ads_store: root_store.my_ads_store, @@ -37,6 +39,7 @@ export const useStores = () => { order_store: root_store.order_store, order_details_store: root_store.order_details_store, sendbird_store: root_store.sendbird_store, + floating_rate_store: root_store.floating_rate_store, }); } return React.useContext(stores_context); diff --git a/packages/p2p/src/stores/my-ads-store.js b/packages/p2p/src/stores/my-ads-store.js index aa3e161373d1..f1c54965e0b9 100644 --- a/packages/p2p/src/stores/my-ads-store.js +++ b/packages/p2p/src/stores/my-ads-store.js @@ -1,10 +1,11 @@ -import { action, observable } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { getDecimalPlaces } from '@deriv/shared'; import { localize } from 'Components/i18next'; import { buy_sell } from 'Constants/buy-sell'; +import { ad_type } from 'Constants/floating-rate'; import BaseStore from 'Stores/base_store'; import { countDecimalPlaces } from 'Utils/string'; -import { decimalValidator, lengthValidator, textValidator } from 'Utils/validations'; +import { decimalValidator, lengthValidator, rangeValidator, textValidator } from 'Utils/validations'; import { requestWS } from 'Utils/websocket'; export default class MyAdsStore extends BaseStore { @@ -22,7 +23,6 @@ export default class MyAdsStore extends BaseStore { @observable edit_ad_form_error = ''; @observable error_message = ''; @observable has_more_items_to_load = false; - @observable has_missing_payment_methods = false; @observable is_ad_created_modal_visible = false; @observable is_ad_exceeds_daily_limit_modal_open = false; @observable is_api_error_modal_visible = false; @@ -33,20 +33,27 @@ export default class MyAdsStore extends BaseStore { @observable is_quick_add_modal_open = false; @observable is_table_loading = false; @observable is_loading = false; + @observable is_switch_modal_open = false; @observable item_offset = 0; @observable p2p_advert_information = {}; + @observable show_ad_form = false; @observable selected_ad_id = ''; @observable selected_advert = null; @observable should_show_add_payment_method = false; @observable should_show_add_payment_method_modal = false; - @observable show_ad_form = false; @observable show_edit_ad_form = false; @observable update_payment_methods_error_message = ''; + @observable required_ad_type; @observable error_code = ''; payment_method_ids = []; payment_method_names = []; + @computed + get selected_ad_type() { + return this.p2p_advert_information.rate_type; + } + @action.bound getAccountStatus() { this.setIsLoading(true); @@ -70,40 +77,45 @@ export default class MyAdsStore extends BaseStore { @action.bound getAdvertInfo() { this.setIsFormLoading(true); - requestWS({ p2p_advert_info: 1, id: this.selected_ad_id, - }).then(response => { - if (!response.error) { - const { p2p_advert_info } = response; - if (!p2p_advert_info.payment_method_names) - p2p_advert_info.payment_method_names = this.payment_method_names; - if (!p2p_advert_info.payment_method_details) - p2p_advert_info.payment_method_details = this.payment_method_details; - this.setP2pAdvertInformation(p2p_advert_info); - } - this.setIsFormLoading(false); - }); + }) + .then(response => { + if (response) { + if (!response.error) { + const { p2p_advert_info } = response; + if (!p2p_advert_info.payment_method_names) + p2p_advert_info.payment_method_names = this.payment_method_names; + if (!p2p_advert_info.payment_method_details) + p2p_advert_info.payment_method_details = this.payment_method_details; + this.setP2pAdvertInformation(p2p_advert_info); + } else { + this.setApiErrorMessage(response.error.message); + } + } + }) + .finally(() => this.setIsFormLoading(false)); } @action.bound getAdvertiserInfo() { this.setIsFormLoading(true); - requestWS({ p2p_advertiser_info: 1, }).then(response => { - if (!response.error) { - const { p2p_advertiser_info } = response; - this.setContactInfo(p2p_advertiser_info.contact_info); - this.setDefaultAdvertDescription(p2p_advertiser_info.default_advert_description); - this.setAvailableBalance(p2p_advertiser_info.balance_available); - } else { - this.setContactInfo(''); - this.setDefaultAdvertDescription(''); + if (response) { + if (!response.error) { + const { p2p_advertiser_info } = response; + this.setContactInfo(p2p_advertiser_info.contact_info); + this.setDefaultAdvertDescription(p2p_advertiser_info.default_advert_description); + this.setAvailableBalance(p2p_advertiser_info.balance_available); + } else { + this.setContactInfo(''); + this.setDefaultAdvertDescription(''); + } + this.setIsFormLoading(false); } - this.setIsFormLoading(false); }); } @@ -134,7 +146,8 @@ export default class MyAdsStore extends BaseStore { amount: Number(values.offer_amount), max_order_amount: Number(values.max_transaction), min_order_amount: Number(values.min_transaction), - rate: Number(values.price_rate), + rate_type: this.root_store.floating_rate_store.rate_type, + rate: Number(values.rate_type), ...(this.payment_method_names.length > 0 && !is_sell_ad ? { payment_method_names: this.payment_method_names } : {}), @@ -241,24 +254,25 @@ export default class MyAdsStore extends BaseStore { } @action.bound - onClickEdit(id) { + onClickEdit(id, rate_type) { if (!this.root_store.general_store.is_barred) { this.setSelectedAdId(id); - this.setShowEditAdForm(true); + this.setRequiredAdType(rate_type); this.getAdvertInfo(); + this.setShowEditAdForm(true); } } @action.bound onClickSaveEditAd(values, { setSubmitting }) { const is_sell_ad = values.type === buy_sell.SELL; - const update_advert = { p2p_advert_update: 1, id: this.selected_ad_id, max_order_amount: Number(values.max_transaction), min_order_amount: Number(values.min_transaction), - rate: Number(values.price_rate), + rate_type: this.required_ad_type, + rate: Number(values.rate_type), ...(this.payment_method_names.length > 0 && !is_sell_ad ? { payment_method_names: this.payment_method_names } : {}), @@ -274,16 +288,21 @@ export default class MyAdsStore extends BaseStore { if (values.description) { update_advert.description = values.description; } + if (this.root_store.floating_rate_store.reached_target_date) { + update_advert.is_active = values.is_active; + } requestWS(update_advert).then(response => { // If there's an error, let the user submit the form again. - if (response && response.error) { - setSubmitting(false); - this.setApiErrorCode(response.error.code); - this.setEditAdFormError(response.error.message); - this.setIsEditAdErrorModalVisible(true); - } else { - this.setShowEditAdForm(false); + if (response) { + if (response.error) { + setSubmitting(false); + this.setApiErrorCode(response.error.code); + this.setEditAdFormError(response.error.message); + this.setIsEditAdErrorModalVisible(true); + } else { + this.setShowEditAdForm(false); + } } }); } @@ -318,22 +337,31 @@ export default class MyAdsStore extends BaseStore { this.setIsTableLoading(true); this.setApiErrorMessage(''); } - - const { list_item_limit } = this.root_store.general_store; - + const { floating_rate_store, general_store } = this.root_store; return new Promise(resolve => { requestWS({ p2p_advertiser_adverts: 1, offset: startIndex, - limit: list_item_limit, + limit: general_store.list_item_limit, }).then(response => { if (!response.error) { const { list } = response.p2p_advertiser_adverts; - this.setHasMoreItemsToLoad(list.length >= list_item_limit); + this.setHasMoreItemsToLoad(list.length >= general_store.list_item_limit); this.setAdverts(this.adverts.concat(list)); - this.setMissingPaymentMethods(!!list.find(payment_method => !payment_method.payment_method_names)); + if (!floating_rate_store.change_ad_alert) { + let should_update_ads = false; + if (floating_rate_store.rate_type === ad_type.FLOAT) { + // Check if there are any Fixed rate ads + should_update_ads = list.some(ad => ad.rate_type === ad_type.FIXED); + floating_rate_store.setChangeAdAlert(should_update_ads); + } else if (floating_rate_store.rate_type === ad_type.FIXED) { + // Check if there are any Float rate ads + should_update_ads = list.some(ad => ad.rate_type === ad_type.FLOAT); + floating_rate_store.setChangeAdAlert(should_update_ads); + } + } } else if (response.error.code === 'PermissionDenied') { - this.root_store.general_store.setIsBlocked(true); + general_store.setIsBlocked(true); } else { this.setApiErrorMessage(response.error.message); } @@ -345,10 +373,9 @@ export default class MyAdsStore extends BaseStore { } @action.bound - restrictLength = (e, handleChange) => { + restrictLength = (e, handleChange, max_characters = 15) => { // typing more than 15 characters will break the layout // max doesn't disable typing, so we will use this to restrict length - const max_characters = 15; if (e.target.value.length > max_characters) { e.target.value = e.target.value.slice(0, max_characters); return; @@ -356,6 +383,18 @@ export default class MyAdsStore extends BaseStore { handleChange(e); }; + @action.bound + restrictDecimalPlace = (e, handleChangeCallback) => { + const pattern = new RegExp(/^[+-]?\d{0,4}(\.\d{0,2})?$/); + if (e.target.value.length > 8) { + e.target.value = e.target.value.slice(0, 8); + return; + } + if (pattern.test(e.target.value)) { + handleChangeCallback(e); + } + }; + @action.bound showQuickAddModal(advert) { this.setSelectedAdId(advert); @@ -437,11 +476,6 @@ export default class MyAdsStore extends BaseStore { this.has_more_items_to_load = has_more_items_to_load; } - @action.bound - setMissingPaymentMethods(has_missing_payment_methods) { - this.has_missing_payment_methods = has_missing_payment_methods; - } - @action.bound setIsAdCreatedModalVisible(is_ad_created_modal_visible) { this.is_ad_created_modal_visible = is_ad_created_modal_visible; @@ -530,6 +564,20 @@ export default class MyAdsStore extends BaseStore { @action.bound setShowEditAdForm(show_edit_ad_form) { this.show_edit_ad_form = show_edit_ad_form; + if (!this.show_edit_ad_form) { + // this.setRequiredAdType(null); + } + } + + @action.bound + setIsSwitchModalOpen(is_switch_modal_open, ad_id) { + this.setSelectedAdId(ad_id); + this.getAdvertInfo(); + this.is_switch_modal_open = is_switch_modal_open; + } + @action.bound + setRequiredAdType(change_ad_type) { + this.required_ad_type = change_ad_type; } @action.bound @@ -539,6 +587,7 @@ export default class MyAdsStore extends BaseStore { @action.bound validateCreateAdForm(values) { + const { general_store, floating_rate_store } = this.root_store; const validations = { default_advert_description: [v => !v || lengthValidator(v), v => !v || textValidator(v)], max_transaction: [ @@ -547,7 +596,7 @@ export default class MyAdsStore extends BaseStore { v => v > 0 && decimalValidator(v) && - countDecimalPlaces(v) <= getDecimalPlaces(this.root_store.general_store.client.currency), + countDecimalPlaces(v) <= getDecimalPlaces(general_store.client.currency), v => (values.offer_amount ? +v <= values.offer_amount : true), v => (values.min_transaction ? +v >= values.min_transaction : true), ], @@ -557,7 +606,7 @@ export default class MyAdsStore extends BaseStore { v => v > 0 && decimalValidator(v) && - countDecimalPlaces(v) <= getDecimalPlaces(this.root_store.general_store.client.currency), + countDecimalPlaces(v) <= getDecimalPlaces(general_store.client.currency), v => (values.offer_amount ? +v <= values.offer_amount : true), v => (values.max_transaction ? +v <= values.max_transaction : true), ], @@ -568,17 +617,23 @@ export default class MyAdsStore extends BaseStore { v => v > 0 && decimalValidator(v) && - countDecimalPlaces(v) <= getDecimalPlaces(this.root_store.general_store.client.currency), + countDecimalPlaces(v) <= getDecimalPlaces(general_store.client.currency), v => (values.min_transaction ? +v >= values.min_transaction : true), v => (values.max_transaction ? +v >= values.max_transaction : true), ], - price_rate: [ + rate_type: [ v => !!v, v => !isNaN(v), v => - v > 0 && - decimalValidator(v) && - countDecimalPlaces(v) <= this.root_store.general_store.client.local_currency_config.decimal_places, + floating_rate_store.rate_type === ad_type.FIXED + ? v > 0 && + decimalValidator(v) && + countDecimalPlaces(v) <= general_store.client.local_currency_config.decimal_places + : true, + v => + floating_rate_store.rate_type === ad_type.FLOAT + ? rangeValidator(parseFloat(v), floating_rate_store.float_rate_offset_limit) + : true, ], }; @@ -592,7 +647,9 @@ export default class MyAdsStore extends BaseStore { max_transaction: localize('Max limit'), min_transaction: localize('Min limit'), offer_amount: localize('Amount'), - price_rate: localize('Fixed rate'), + payment_info: localize('Payment instructions'), + rate_type: + floating_rate_store.rate_type === ad_type.FLOAT ? localize('Floating rate') : localize('Fixed rate'), }; const getCommonMessages = field_name => [localize('{{field_name}} is required', { field_name })]; @@ -643,6 +700,9 @@ export default class MyAdsStore extends BaseStore { localize('{{field_name}} is required', { field_name }), localize('Enter a valid amount'), localize('Enter a valid amount'), + localize("Enter a value thats's within -{{limit}}% to +{{limit}}%", { + limit: floating_rate_store.float_rate_offset_limit, + }), ]; const errors = {}; @@ -666,7 +726,7 @@ export default class MyAdsStore extends BaseStore { case 'min_transaction': errors[key] = getMinTransactionLimitMessages(mapped_key[key])[error_index]; break; - case 'price_rate': + case 'rate_type': errors[key] = getPriceRateMessages(mapped_key[key])[error_index]; break; default: @@ -686,6 +746,7 @@ export default class MyAdsStore extends BaseStore { @action.bound validateEditAdForm(values) { + const { general_store, floating_rate_store } = this.root_store; const validations = { description: [v => !v || lengthValidator(v), v => !v || textValidator(v)], max_transaction: [ @@ -694,7 +755,7 @@ export default class MyAdsStore extends BaseStore { v => v > 0 && decimalValidator(v) && - countDecimalPlaces(v) <= getDecimalPlaces(this.root_store.general_store.client.currency), + countDecimalPlaces(v) <= getDecimalPlaces(general_store.client.currency), v => (values.offer_amount ? +v <= values.offer_amount : true), v => (values.min_transaction ? +v >= values.min_transaction : true), ], @@ -704,29 +765,23 @@ export default class MyAdsStore extends BaseStore { v => v > 0 && decimalValidator(v) && - countDecimalPlaces(v) <= getDecimalPlaces(this.root_store.general_store.client.currency), + countDecimalPlaces(v) <= getDecimalPlaces(general_store.client.currency), v => (values.offer_amount ? +v <= values.offer_amount : true), v => (values.max_transaction ? +v <= values.max_transaction : true), ], - // Offer amount disabled for edit ads - // offer_amount: [ - // v => !!v, - // v => !isNaN(v), - // v => (values.type === buy_sell.SELL ? v <= this.available_balance : !!v), - // v => - // v > 0 && - // decimalValidator(v) && - // countDecimalPlaces(v) <= getDecimalPlaces(this.root_store.general_store.client.currency), - // v => (values.min_transaction ? +v >= values.min_transaction : true), - // v => (values.max_transaction ? +v >= values.max_transaction : true), - // ], - price_rate: [ + rate_type: [ v => !!v, v => !isNaN(v), v => - v > 0 && - decimalValidator(v) && - countDecimalPlaces(v) <= this.root_store.general_store.client.local_currency_config.decimal_places, + this.required_ad_type === ad_type.FIXED + ? v > 0 && + decimalValidator(v) && + countDecimalPlaces(v) <= general_store.client.local_currency_config.decimal_places + : true, + v => + this.required_ad_type === ad_type.FLOAT + ? rangeValidator(v, parseFloat(floating_rate_store.float_rate_offset_limit)) + : true, ], }; @@ -740,7 +795,7 @@ export default class MyAdsStore extends BaseStore { max_transaction: localize('Max limit'), min_transaction: localize('Min limit'), offer_amount: localize('Amount'), - price_rate: localize('Fixed rate'), + rate_type: this.required_ad_type === ad_type.FLOAT ? localize('Floating rate') : localize('Fixed rate'), }; const getCommonMessages = field_name => [localize('{{field_name}} is required', { field_name })]; @@ -762,15 +817,6 @@ export default class MyAdsStore extends BaseStore { ), ]; - // const getOfferAmountMessages = field_name => [ - // localize('{{field_name}} is required', { field_name }), - // localize('Enter a valid amount'), - // localize('Max available amount is {{value}}', { value: this.available_balance }), - // localize('Enter a valid amount'), - // localize('{{field_name}} should not be below Min limit', { field_name }), - // localize('{{field_name}} should not be below Max limit', { field_name }), - // ]; - const getMaxTransactionLimitMessages = field_name => [ localize('{{field_name}} is required', { field_name }), localize('Enter a valid amount'), @@ -791,6 +837,9 @@ export default class MyAdsStore extends BaseStore { localize('{{field_name}} is required', { field_name }), localize('Enter a valid amount'), localize('Enter a valid amount'), + localize("Enter a value thats's within -{{limit}}% to +{{limit}}%", { + limit: floating_rate_store.float_rate_offset_limit, + }), ]; const errors = {}; @@ -805,16 +854,13 @@ export default class MyAdsStore extends BaseStore { case 'description': errors[key] = getDefaultAdvertDescriptionMessages(mapped_key[key])[error_index]; break; - // case 'offer_amount': - // errors[key] = getOfferAmountMessages(mapped_key[key])[error_index]; - // break; case 'max_transaction': errors[key] = getMaxTransactionLimitMessages(mapped_key[key])[error_index]; break; case 'min_transaction': errors[key] = getMinTransactionLimitMessages(mapped_key[key])[error_index]; break; - case 'price_rate': + case 'rate_type': errors[key] = getPriceRateMessages(mapped_key[key])[error_index]; break; default: @@ -831,4 +877,12 @@ export default class MyAdsStore extends BaseStore { return errors; } + + toggleMyAdsRateSwitchModal(change_ad_type, is_open_edit_form) { + this.setRequiredAdType(change_ad_type); + if (is_open_edit_form) { + this.setShowEditAdForm(true); + } + this.setIsSwitchModalOpen(false, this.selected_ad_id); + } } diff --git a/packages/p2p/src/utils/format-value.js b/packages/p2p/src/utils/format-value.js new file mode 100644 index 000000000000..47acfaaaff11 --- /dev/null +++ b/packages/p2p/src/utils/format-value.js @@ -0,0 +1,84 @@ +import { ad_type } from 'Constants/floating-rate'; +import { formatMoney } from '@deriv/shared'; + +export const roundOffDecimal = (number, decimal_place = 2) => { + // Rounds of the digit to the specified decimal place + if (number === null || number === undefined) { + return 0; + } + + return parseFloat(number).toFixed(decimal_place); + // TODO: Uncomment the below line once BE has resolved the rounding issue + // return parseFloat(Math.round(number * Math.pow(10, decimal_place)) / Math.pow(10, decimal_place)); +}; + +export const setDecimalPlaces = (value, expected_decimal_place) => { + // Returns the accurate number of decimal places to prevent trailing zeros + if (!value?.toString()) { + return 0; + } + const actual_decimal_place = value.toString().split('.')[1]?.length; + return actual_decimal_place > expected_decimal_place ? expected_decimal_place : actual_decimal_place; +}; + +export const percentOf = (number, percent) => { + // This method is used for computing the effective percent of a number + const parsed_number = parseFloat(number); + const parsed_percent = parseFloat(percent); + return parsed_number + parsed_number * (parsed_percent / 100); +}; + +export const generateEffectiveRate = ({ + price = 0, + rate = 0, + local_currency = {}, + exchange_rate = 0, + rate_type = ad_type.FIXED, +} = {}) => { + let effective_rate = 0; + let display_effective_rate = 0; + if (rate_type === ad_type.FIXED) { + effective_rate = price; + display_effective_rate = formatMoney(local_currency, effective_rate, true); + } else { + effective_rate = percentOf(exchange_rate, rate); + const decimal_place = setDecimalPlaces(effective_rate, 6); + display_effective_rate = removeTrailingZeros( + formatMoney(local_currency, roundOffDecimal(effective_rate, decimal_place), true, decimal_place) + ); + } + return { effective_rate, display_effective_rate }; +}; + +export const removeTrailingZeros = value => { + // Returns the string after truncating extra zeros + if (!value) { + return ''; + } + const [input, unit] = value.toString().trim().split(' '); + if (input.indexOf('.') === -1) { + return formatInput(input, unit); + } + let trim_from = input.length - 1; + do { + if (input[trim_from] === '0') { + trim_from--; + } + } while (input[trim_from] === '0'); + if (input[trim_from] === '.') { + trim_from--; + } + const result = value.toString().substr(0, trim_from + 1); + return formatInput(result, unit); +}; + +const formatInput = (input, unit) => { + const plain_input = input.replace(/,/g, ''); + if (parseFloat(plain_input) % 1 === 0) { + return `${input}.00 ${unit ? unit.trim() : ''}`; + } + if (plain_input.split('.')[1].length === 1) { + return `${input}0 ${unit ? unit.trim() : ''}`; + } + return `${input}${unit ? ` ${unit.trim()}` : ''}`; +}; diff --git a/packages/p2p/src/utils/orders.js b/packages/p2p/src/utils/orders.js index 0ba7ad9f4894..4756ffe70c6c 100644 --- a/packages/p2p/src/utils/orders.js +++ b/packages/p2p/src/utils/orders.js @@ -237,7 +237,7 @@ export default class ExtendedOrderDetails { if (this.is_buyer_confirmed_order) { const confirm_payment = localize('Confirm payment'); - const wait_for_release = localize('Wait for release'); + const wait_for_release = localize('Waiting for the seller to confirm'); if (this.is_my_ad) { return this.is_buy_order ? confirm_payment : wait_for_release; diff --git a/packages/p2p/src/utils/validations.js b/packages/p2p/src/utils/validations.js index 2933511565d9..34bb405ccbfb 100644 --- a/packages/p2p/src/utils/validations.js +++ b/packages/p2p/src/utils/validations.js @@ -4,6 +4,9 @@ export const lengthValidator = v => v.length >= 1 && v.length <= 300; export const textValidator = v => /^[\p{L}\p{Nd}\s'.,:;()@#+/-]*$/u.test(v); +// Validates if the given value falls within the set range and returns a boolean +export const rangeValidator = (input, limit) => input >= limit * -1 && input <= limit; + // validates floating-point integers in input box that do not contain scientific notation (e, E, -, +) such as 12.2e+2 or 12.2e-2 and no negative numbers export const floatingPointValidator = v => ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', '.'].includes(v) || /^[0-9]*[.]?[0-9]+$(?:[eE\-+]*$)/.test(v); diff --git a/packages/shared/src/styles/constants.scss b/packages/shared/src/styles/constants.scss index 686fc0b7bdd2..f674dd59b436 100644 --- a/packages/shared/src/styles/constants.scss +++ b/packages/shared/src/styles/constants.scss @@ -59,6 +59,7 @@ $alpha-color-black-5: transparentize($color-black-7, 0.16); $alpha-color-black-6: transparentize($color-black-7, 0.36); $alpha-color-blue-1: transparentize($color-blue, 0.84); $alpha-color-blue-2: transparentize($color-blue-3, 0.84); +$alpha-color-blue-3: transparentize($color-blue, 0.92); $alpha-color-white-1: transparentize($color-white, 0.04); $alpha-color-white-2: transparentize($color-white, 0.84); $alpha-color-white-3: transparentize($color-white, 0.92); diff --git a/packages/shared/src/styles/themes.scss b/packages/shared/src/styles/themes.scss index 901ee03e4f8e..9bb62beae86b 100644 --- a/packages/shared/src/styles/themes.scss +++ b/packages/shared/src/styles/themes.scss @@ -71,6 +71,7 @@ --general-section-2: #{$color-grey-2}; --general-section-3: #{$color-grey-11}; --general-section-4: #{$color-grey-12}; + --general-section-5: #{$color-grey-2}; --general-disabled: #{$color-grey-3}; --general-hover: #{$color-grey-4}; --general-active: #{$color-grey-5}; @@ -88,6 +89,8 @@ --icon-light-background: #{$color-black-9}; --icon-dark-background: #{$color-white}; --icon-grey-background: #{$color-grey-2}; + --text-status-info-blue: #{$color-blue}; + --text-hint: #{$color-black-1}; // Purchase --purchase-main-1: #{$color-green-1}; --purchase-section-1: #{$color-green-2}; @@ -161,6 +164,7 @@ // Transparentize --transparent-success: #{$alpha-color-green-1}; --transparent-info: #{$alpha-color-blue-1}; + --transparent-hint: #{$alpha-color-blue-3}; --transparent-danger: #{$alpha-color-red-2}; /* TODO: change to styleguide later */ // Gradient @@ -186,6 +190,7 @@ --general-section-3: #{$color-black-5}; // @TODO: get color from design --general-section-4: #{$color-black-5}; + --general-section-5: #{$color-black-5}; --general-disabled: #{$color-black-4}; --general-hover: #{$color-black-5}; --general-active: #{$color-black-8}; @@ -198,6 +203,8 @@ --text-loss-danger: #{$color-red-2}; --text-red: #{$color-red}; --text-colored-background: #{$color-white}; + --text-status-info-blue: #{$color-blue}; + --text-hint: #{$color-grey}; --icon-light-background: #{$color-black-9}; --icon-dark-background: #{$color-white}; --icon-grey-background: #{$color-black-1}; @@ -230,6 +237,7 @@ --state-hover: #{$color-black-5}; --state-active: #{$color-black-8}; --state-disabled: #{$color-black-4}; + --checkbox-disabled-grey: #{$color-grey-6}; // Border --border-normal: #{$color-black-8}; --border-normal-1: #{$color-grey-5}; @@ -247,7 +255,7 @@ --status-adjustment: #{$color-grey-1}; --status-danger: #{$color-red-2}; --status-warning: #{$color-yellow}; - --status-warning-2: #{$alpha-color-yellow-1}; + --status-warning-transparent: #{$alpha-color-yellow-1}; --status-success: #{$color-green-3}; --status-transfer: #{$color-orange}; --status-info: #{$color-blue}; @@ -255,6 +263,7 @@ // Transparentize --transparent-success: #{$alpha-color-green-2}; --transparent-info: #{$alpha-color-blue-1}; + --transparent-hint: #{$alpha-color-blue-1}; --transparent-danger: #{$alpha-color-red-2}; /* TODO: change to styleguide later */ // Gradient diff --git a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx index 5a871fee2e9e..d45519dd7ba2 100644 --- a/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx +++ b/packages/trader/src/App/Components/Elements/ContractDrawer/contract-drawer-card.jsx @@ -2,12 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import { DesktopWrapper, MobileWrapper, Collapsible, ContractCard, useHover } from '@deriv/components'; -import { - isCryptoContract, - isDesktop, - getEndTime, - getSymbolDisplayName, -} from '@deriv/shared'; +import { isCryptoContract, isDesktop, getEndTime, getSymbolDisplayName } from '@deriv/shared'; import { getCardLabels, getContractTypeDisplay } from 'Constants/contract'; import { connect } from 'Stores/connect'; import { getMarketInformation } from 'Utils/Helpers/market-underlying'; diff --git a/packages/trader/src/App/Components/Elements/Errors/index.js b/packages/trader/src/App/Components/Elements/Errors/index.js index 972faa40fa1f..7673d0bd01f2 100644 --- a/packages/trader/src/App/Components/Elements/Errors/index.js +++ b/packages/trader/src/App/Components/Elements/Errors/index.js @@ -1,3 +1,3 @@ import ErrorComponent from './error-component.jsx'; -export default ErrorComponent; +export default ErrorComponent;