{!!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 (
{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',
+ })}
+
+
{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 (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default observer(RateChangeModal);
diff --git a/packages/p2p/src/components/buy-sell/rate-change-modal.scss b/packages/p2p/src/components/buy-sell/rate-change-modal.scss
new file mode 100644
index 000000000000..735d47d5395f
--- /dev/null
+++ b/packages/p2p/src/components/buy-sell/rate-change-modal.scss
@@ -0,0 +1,5 @@
+.dc-modal__container_rate-changed-modal {
+ @include mobile {
+ min-width: 328px;
+ }
+}
diff --git a/packages/p2p/src/components/floating-rate/__test__/floating-rate.spec.js b/packages/p2p/src/components/floating-rate/__test__/floating-rate.spec.js
new file mode 100644
index 000000000000..8cbd16743ff7
--- /dev/null
+++ b/packages/p2p/src/components/floating-rate/__test__/floating-rate.spec.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import FloatingRate from '../floating-rate.jsx';
+
+jest.mock('Stores', () => ({
+ ...jest.requireActual('Stores'),
+ useStores: jest.fn().mockReturnValue({
+ general_store: {
+ current_focus: '',
+ client: { local_currency_config: { decimal_places: 2 } },
+ setCurrentFocus: jest.fn(),
+ },
+ floating_rate_store: {
+ exchange_rate: '100',
+ },
+ }),
+}));
+
+describe('
', () => {
+ it('should render default state of the component with hint message and increment, decrement buttons', () => {
+ render(
);
+
+ expect(screen.getByText('of the market rate')).toBeInTheDocument();
+ expect(screen.getAllByRole('button').length).toBe(2);
+ });
+
+ it('should display error messages when error is passed as props', () => {
+ render(
);
+
+ expect(screen.getByText('Floating rate error')).toBeInTheDocument();
+ });
+
+ it('should render market rate feed based on the floating rate value passed', () => {
+ render(
);
+
+ expect(screen.getByText('Your rate is = 102.00')).toBeInTheDocument();
+ });
+
+ it('should render the exchange rate in hint', () => {
+ render(
);
+
+ expect(screen.getByText('1 AED = 100.00 INR')).toBeInTheDocument();
+ });
+});
diff --git a/packages/p2p/src/components/floating-rate/floating-rate.jsx b/packages/p2p/src/components/floating-rate/floating-rate.jsx
new file mode 100644
index 000000000000..47f23db1dab6
--- /dev/null
+++ b/packages/p2p/src/components/floating-rate/floating-rate.jsx
@@ -0,0 +1,142 @@
+import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { InputField, Text } from '@deriv/components';
+import { formatMoney, isMobile, mobileOSDetect } from '@deriv/shared';
+import { localize } from 'Components/i18next';
+import { useStores } from 'Stores';
+import { setDecimalPlaces, removeTrailingZeros, percentOf, roundOffDecimal } from 'Utils/format-value.js';
+import './floating-rate.scss';
+
+const FloatingRate = ({
+ change_handler,
+ className,
+ error_messages,
+ fiat_currency,
+ local_currency,
+ onChange,
+ offset,
+ data_testid,
+ ...props
+}) => {
+ const { floating_rate_store, general_store } = useStores();
+ const os = mobileOSDetect();
+ const { name, value, required } = props;
+
+ const market_feed = value ? percentOf(floating_rate_store.exchange_rate, value) : floating_rate_store.exchange_rate;
+ const decimal_place = setDecimalPlaces(market_feed, 6);
+
+ // Input mask for formatting value on blur of floating rate field
+ const onBlurHandler = e => {
+ let float_rate = e.target.value;
+ if (!isNaN(float_rate) && float_rate.trim().length) {
+ float_rate = parseFloat(float_rate).toFixed(2);
+ if (/^\d+/.test(float_rate) && float_rate > 0) {
+ // Assign + symbol for positive rate
+ e.target.value = `+${float_rate}`;
+ } else {
+ e.target.value = float_rate;
+ }
+ }
+ onChange(e);
+ };
+ return (
+
+
+
+ {localize('at')}
+
+
+
+
+ {localize('of the market rate')}
+
+
+ 1 {fiat_currency} ={' '}
+ {removeTrailingZeros(formatMoney(local_currency, floating_rate_store.exchange_rate, true, 6))}{' '}
+ {local_currency}
+
+
+
+ {error_messages ? (
+
+ {error_messages}
+
+ ) : (
+
+ {localize('Your rate is')} ={' '}
+ {removeTrailingZeros(
+ formatMoney(local_currency, roundOffDecimal(market_feed, decimal_place), true, decimal_place)
+ )}{' '}
+ {local_currency}
+
+ )}
+
+ );
+};
+
+FloatingRate.propTypes = {
+ change_handler: PropTypes.func,
+ className: PropTypes.string,
+ error_messages: PropTypes.string,
+ fiat_currency: PropTypes.string,
+ local_currency: PropTypes.string,
+ onChange: PropTypes.func,
+ offset: PropTypes.object,
+};
+
+export default observer(FloatingRate);
diff --git a/packages/p2p/src/components/floating-rate/floating-rate.scss b/packages/p2p/src/components/floating-rate/floating-rate.scss
new file mode 100644
index 000000000000..97fa6ffeb1de
--- /dev/null
+++ b/packages/p2p/src/components/floating-rate/floating-rate.scss
@@ -0,0 +1,222 @@
+.floating-rate {
+ display: flex;
+ flex-direction: column;
+ &__field {
+ display: flex;
+ align-items: center;
+
+ @include mobile {
+ margin-top: -3rem;
+ }
+
+ &--prefix {
+ margin-right: 2rem;
+ }
+ }
+ &__input {
+ height: 4rem;
+ align-self: center;
+ appearance: none;
+ box-sizing: border-box;
+ border-radius: $BORDER_RADIUS;
+ color: var(--text-general);
+ background-image: none;
+ text-overflow: ellipsis;
+ @include mobile {
+ padding: 0;
+ }
+
+ &--error-field {
+ color: $color-red-1;
+ border-color: $color-red;
+ }
+ }
+
+ &__percent {
+ order: 3;
+ background: transparent;
+ border-color: transparent;
+ padding: 0 0.2rem;
+ color: inherit;
+ @include mobile {
+ position: relative;
+ right: 9rem;
+ }
+
+ &--symbol {
+ font-size: 1.4rem;
+ line-height: 1.5;
+ }
+ &:before {
+ @include typeface(--paragraph-center-normal-black);
+ color: inherit;
+ }
+ }
+
+ &__mkt-rate {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: flex-start;
+ padding: 0.4rem 0.6rem;
+ position: static;
+ left: 10rem;
+ top: 0px;
+ background-color: var(--transparent-hint);
+ border-radius: 0px 4px 4px 0px;
+ /* Inside auto layout */
+ flex: none;
+ order: 1;
+ align-self: stretch;
+ flex-grow: 1;
+ gap: 0.2rem;
+
+ @include mobile {
+ flex-direction: row;
+ align-items: center;
+ gap: 1rem;
+ }
+
+ &--label {
+ position: static;
+ font-style: normal;
+ flex: none;
+ order: 1;
+ flex-grow: 0;
+ margin: 0px;
+ }
+
+ &--msg {
+ position: static;
+ height: 18px;
+ left: 8px;
+ top: 18px;
+ font-style: normal;
+ flex: none;
+ order: 2;
+ flex-grow: 0;
+ margin: 0px;
+ }
+ }
+
+ &__hint {
+ padding-left: 4rem;
+ text-transform: none;
+ margin-top: 0.3rem;
+ @include mobile {
+ padding-left: 1rem;
+ }
+ }
+
+ &__error-message {
+ padding-left: 4rem;
+ text-transform: none;
+ @include mobile {
+ padding-left: 1rem;
+ }
+ }
+
+ .dc-input-wrapper {
+ display: flex;
+ align-items: center;
+ border: 1px solid var(--border-normal);
+ border-radius: 4px;
+
+ &--error {
+ border: 1px solid $color-red;
+ }
+
+ .dc-input-wrapper__button {
+ top: unset;
+ &--increment {
+ right: unset;
+ order: 4;
+ }
+
+ &--decrement {
+ left: unset;
+ }
+ }
+
+ .input {
+ text-align: right;
+ border: unset;
+ background-color: unset;
+ padding: unset;
+ &:focus {
+ border-color: unset;
+ }
+ &:hover {
+ border-color: unset;
+ }
+ &--has-inline-prefix {
+ padding-right: unset !important;
+ }
+ @include mobile {
+ text-align: center;
+ }
+ }
+
+ &:hover {
+ border-color: var(--border-hover);
+ }
+ &:active,
+ &:focus {
+ border-color: var(--border-active);
+ }
+
+ button.dc-input-wrapper__button {
+ position: inherit !important;
+ @include mobile {
+ // In some browsers the background color for button remains set as they consider hover as focus
+ &:hover {
+ background-color: unset !important;
+ }
+
+ &:active {
+ background-color: var(--state-hover) !important;
+ }
+ }
+ }
+
+ &--error:hover {
+ border-color: $color-red !important;
+ }
+ }
+
+ .dc-input-suffix {
+ display: flex;
+ align-items: center;
+
+ @include mobile {
+ width: -webkit-fill-available;
+ }
+ }
+}
+
+.dc-input-wrapper__button,
+button.dc-input-wrapper__button {
+ top: 0.6rem;
+ z-index: auto;
+}
+
+.dc-input-field {
+ @include mobile {
+ width: 100%;
+ }
+
+ @include desktop {
+ margin: unset;
+ }
+}
+
+.mobile-layout {
+ display: flex;
+ flex-direction: column;
+}
+
+.p2p-my-ads__form-field {
+ @include mobile {
+ height: auto !important;
+ }
+}
diff --git a/packages/p2p/src/components/floating-rate/index.js b/packages/p2p/src/components/floating-rate/index.js
new file mode 100644
index 000000000000..d7cee0ed00de
--- /dev/null
+++ b/packages/p2p/src/components/floating-rate/index.js
@@ -0,0 +1,4 @@
+import FloatingRate from './floating-rate.jsx';
+import './floating-rate.scss';
+
+export default FloatingRate;
diff --git a/packages/p2p/src/components/my-ads/ad-status.scss b/packages/p2p/src/components/my-ads/ad-status.scss
index f2c46b3c0b65..433482265f3b 100644
--- a/packages/p2p/src/components/my-ads/ad-status.scss
+++ b/packages/p2p/src/components/my-ads/ad-status.scss
@@ -1,11 +1,23 @@
.ad-status {
&--active {
- background: rgba(75, 180, 179, 0.16);
- border-radius: 1.6rem;
- padding: 0.2rem 1.6rem;
+ &:before {
+ content: '';
+ height: 100%;
+ width: 100%;
+ background-color: var(--status-success);
+ opacity: 0.16;
+ display: block;
+ position: absolute;
+ left: 0;
+ top: 0;
+ border-radius: 1.6rem;
+ }
+ padding: 0.1rem 1.2rem;
text-align: center;
- width: 8.8rem;
-
+ display: flex;
+ position: relative;
+ width: 6.7rem;
+
@include mobile {
margin-bottom: 0.8rem;
padding: 0.2rem 1rem;
@@ -13,14 +25,22 @@
}
&--inactive {
- border: 1px solid var(--status-danger);
- border-radius: 0.2rem;
+ &:before {
+ content: '';
+ height: 100%;
+ width: 100%;
+ background-color: var(--status-danger);
+ opacity: 0.16;
+ display: block;
+ position: absolute;
+ left: 0;
+ top: 0;
+ border-radius: 1.6rem;
+ }
padding: 0.1rem 1.2rem;
text-align: center;
- width: 8.8rem;
-
- @include mobile {
- margin-bottom: 0.8rem;
- }
+ display: flex;
+ position: relative;
+ width: 8rem;
}
}
diff --git a/packages/p2p/src/components/my-ads/ad-type.jsx b/packages/p2p/src/components/my-ads/ad-type.jsx
new file mode 100644
index 000000000000..64ad1a6c9a06
--- /dev/null
+++ b/packages/p2p/src/components/my-ads/ad-type.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { localize } from 'Components/i18next';
+import { Text } from '@deriv/components';
+import './ad-type.scss';
+
+const AdType = ({ float_rate }) => {
+ return (
+
+
+ {localize('Float')}
+
+
+ {float_rate}%
+
+
+ );
+};
+
+AdType.propTypes = {
+ float_rate: PropTypes.string,
+};
+
+export default AdType;
diff --git a/packages/p2p/src/components/my-ads/ad-type.scss b/packages/p2p/src/components/my-ads/ad-type.scss
new file mode 100644
index 000000000000..f12ea13ea9c6
--- /dev/null
+++ b/packages/p2p/src/components/my-ads/ad-type.scss
@@ -0,0 +1,16 @@
+.ad-type {
+ display: flex;
+ align-items: center;
+ justify-content: space-around;
+
+ &__badge {
+ align-items: center;
+ border-radius: 0.4rem;
+ border: 1px solid var(--border-normal);
+ display: flex;
+ flex-direction: row;
+ margin: 0.25rem;
+ padding: 0.1rem 0.8rem;
+ width: fit-content;
+ }
+}
diff --git a/packages/p2p/src/components/my-ads/buy-ad-payment-methods-list.jsx b/packages/p2p/src/components/my-ads/buy-ad-payment-methods-list.jsx
index 6ed1609a86ed..ae4fae0410e9 100644
--- a/packages/p2p/src/components/my-ads/buy-ad-payment-methods-list.jsx
+++ b/packages/p2p/src/components/my-ads/buy-ad-payment-methods-list.jsx
@@ -7,7 +7,7 @@ import { localize } from 'Components/i18next';
import PropTypes from 'prop-types';
import './buy-ad-payment-methods-list.scss';
-const BuyAdPaymentMethodsList = ({ selected_methods, setSelectedMethods }) => {
+const BuyAdPaymentMethodsList = ({ selected_methods, setSelectedMethods, touched }) => {
const { my_ads_store, my_profile_store } = useStores();
const [selected_edit_method, setSelectedEditMethod] = React.useState();
const [payment_methods_list, setPaymentMethodsList] = React.useState([]);
@@ -33,6 +33,7 @@ const BuyAdPaymentMethodsList = ({ selected_methods, setSelectedMethods }) => {
text: my_profile_store.getPaymentMethodDisplayName(value),
},
]);
+ if (typeof touched === 'function') touched(true);
}
};
@@ -46,6 +47,7 @@ const BuyAdPaymentMethodsList = ({ selected_methods, setSelectedMethods }) => {
...payment_methods_list.filter(payment_method => payment_method.value !== value),
selected_edit_method,
]);
+ if (typeof touched === 'function') touched(true);
}
};
@@ -56,6 +58,7 @@ const BuyAdPaymentMethodsList = ({ selected_methods, setSelectedMethods }) => {
setSelectedMethods([...selected_methods, value]);
setPaymentMethodsList(payment_methods_list.filter(payment_method => payment_method.value !== value));
}
+ if (typeof touched === 'function') touched(true);
}
};
diff --git a/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx b/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx
index 5f5953290ee7..ff5f238c63ee 100644
--- a/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx
+++ b/packages/p2p/src/components/my-ads/create-ad-form-payment-methods.jsx
@@ -42,7 +42,7 @@ const CreateAdFormPaymentMethods = ({ is_sell_advert, onSelectPaymentMethods })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [selected_buy_methods, selected_sell_methods]);
+ }, [is_sell_advert, selected_buy_methods, selected_sell_methods]);
if (is_sell_advert) {
if (my_profile_store.advertiser_has_payment_methods) {
diff --git a/packages/p2p/src/components/my-ads/create-ad-form.jsx b/packages/p2p/src/components/my-ads/create-ad-form.jsx
index be6d585389d8..6fbd5d2f176e 100644
--- a/packages/p2p/src/components/my-ads/create-ad-form.jsx
+++ b/packages/p2p/src/components/my-ads/create-ad-form.jsx
@@ -14,9 +14,11 @@ import {
import { formatMoney, isDesktop, isMobile, mobileOSDetect } from '@deriv/shared';
import { reaction } from 'mobx';
import { observer } from 'mobx-react-lite';
-import { Localize, localize } from 'Components/i18next';
+import FloatingRate from 'Components/floating-rate';
import { useUpdatingAvailableBalance } from 'Components/hooks';
+import { Localize, localize } from 'Components/i18next';
import { buy_sell } from 'Constants/buy-sell';
+import { ad_type } from 'Constants/floating-rate';
import { useStores } from 'Stores';
import CreateAdSummary from './create-ad-summary.jsx';
import CreateAdErrorModal from './create-ad-error-modal.jsx';
@@ -27,19 +29,15 @@ const CreateAdFormWrapper = ({ children }) => {
if (isMobile()) {
return
{children};
}
-
return children;
};
const CreateAdForm = () => {
- const { general_store, my_ads_store, my_profile_store } = useStores();
+ const { floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores();
const available_balance = useUpdatingAvailableBalance();
const os = mobileOSDetect();
-
const { currency, local_currency_config } = general_store.client;
-
const should_not_show_auto_archive_message_again = React.useRef(false);
-
const [selected_methods, setSelectedMethods] = React.useState([]);
// eslint-disable-next-line no-shadow
@@ -65,15 +63,17 @@ const CreateAdForm = () => {
React.useEffect(() => {
my_profile_store.getPaymentMethodsList();
my_profile_store.getAdvertiserPaymentMethods();
-
const disposeApiErrorReaction = reaction(
() => my_ads_store.api_error_message,
() => my_ads_store.setIsApiErrorModalVisible(!!my_ads_store.api_error_message)
);
+ // P2P configuration is not subscribable. Hence need to fetch it on demand
+ general_store.setP2PConfig();
return () => {
disposeApiErrorReaction();
my_ads_store.setApiErrorMessage('');
+ floating_rate_store.setApiErrorMessage('');
};
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -101,7 +101,8 @@ const CreateAdForm = () => {
max_transaction: '',
min_transaction: '',
offer_amount: '',
- price_rate: '',
+ payment_info: my_ads_store.payment_info,
+ rate_type: floating_rate_store.rate_type === ad_type.FLOAT ? '-0.01' : '',
type: buy_sell.BUY,
}}
onSubmit={my_ads_store.handleSubmit}
@@ -114,8 +115,20 @@ const CreateAdForm = () => {
{({ errors, handleChange, isSubmitting, isValid, setFieldValue, touched, values }) => {
const is_sell_advert = values.type === buy_sell.SELL;
+ const onChangeAdTypeHandler = user_input => {
+ if (floating_rate_store.rate_type === ad_type.FLOAT) {
+ if (user_input === buy_sell.SELL) {
+ setFieldValue('rate_type', '+0.01');
+ } else {
+ setFieldValue('rate_type', '-0.01');
+ }
+ }
+
+ setFieldValue('type', user_input);
+ };
+
return (
-
+
@@ -232,6 +277,7 @@ const CreateAdForm = () => {
{
{({ field }) => (
{
{({ field }) => (
{
{({ field }) => (
{
- const { general_store } = useStores();
+ const { floating_rate_store, general_store } = useStores();
const { currency, local_currency_config } = general_store.client;
-
+ const market_feed = floating_rate_store.rate_type === ad_type.FLOAT ? floating_rate_store.exchange_rate : null;
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) : '';
+
+ 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 +55,7 @@ const CreateAdSummary = ({ offer_amount, price_rate, type }) => {
return (
diff --git a/packages/p2p/src/components/my-ads/edit-ad-cancel-modal.jsx b/packages/p2p/src/components/my-ads/edit-ad-cancel-modal.jsx
new file mode 100644
index 000000000000..dea5450dd048
--- /dev/null
+++ b/packages/p2p/src/components/my-ads/edit-ad-cancel-modal.jsx
@@ -0,0 +1,27 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Modal, Text } from '@deriv/components';
+import { localize } from 'Components/i18next';
+
+const EditAdCancelModal = ({ onClick, is_open }) => {
+ return (
+
+
+
+ {localize('If you choose to cancel, the edited details will be lost.')}
+
+
+
+
+
+ );
+};
+
+EditAdCancelModal.propTypes = {
+ is_open: PropTypes.bool,
+ onClick: PropTypes.func,
+};
+
+export default EditAdCancelModal;
diff --git a/packages/p2p/src/components/my-ads/edit-ad-form-payment-methods.jsx b/packages/p2p/src/components/my-ads/edit-ad-form-payment-methods.jsx
index db742843dc29..404700bce205 100644
--- a/packages/p2p/src/components/my-ads/edit-ad-form-payment-methods.jsx
+++ b/packages/p2p/src/components/my-ads/edit-ad-form-payment-methods.jsx
@@ -6,7 +6,7 @@ import { localize } from 'Components/i18next';
import BuyAdPaymentMethodsList from './buy-ad-payment-methods-list';
import SellAdPaymentMethodsList from './sell-ad-payment-methods-list';
-const EditAdFormPaymentMethods = ({ is_sell_advert, selected_methods, setSelectedMethods }) => {
+const EditAdFormPaymentMethods = ({ is_sell_advert, selected_methods, setSelectedMethods, touched }) => {
const { my_ads_store, my_profile_store } = useStores();
const onClickPaymentMethodCard = payment_method => {
@@ -21,6 +21,7 @@ const EditAdFormPaymentMethods = ({ is_sell_advert, selected_methods, setSelecte
);
setSelectedMethods(selected_methods.filter(i => i !== payment_method.ID));
}
+ touched(true);
};
React.useEffect(() => {
@@ -53,7 +54,13 @@ const EditAdFormPaymentMethods = ({ is_sell_advert, selected_methods, setSelecte
);
}
- return
;
+ return (
+
+ );
};
export default observer(EditAdFormPaymentMethods);
diff --git a/packages/p2p/src/components/my-ads/edit-ad-form.jsx b/packages/p2p/src/components/my-ads/edit-ad-form.jsx
index fa7a1b188bca..a46ef27130c3 100644
--- a/packages/p2p/src/components/my-ads/edit-ad-form.jsx
+++ b/packages/p2p/src/components/my-ads/edit-ad-form.jsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import classNames from 'classnames';
import { Formik, Field, Form } from 'formik';
-import { Button, Div100vhContainer, Input, Modal, Text, ThemedScrollbars } from '@deriv/components';
+import { Button, Div100vhContainer, Input, Loading, Modal, Text, ThemedScrollbars } from '@deriv/components';
import { formatMoney, isDesktop, isMobile, mobileOSDetect } from '@deriv/shared';
import { observer } from 'mobx-react-lite';
import { Localize, localize } from 'Components/i18next';
@@ -10,6 +10,9 @@ import PageReturn from 'Components/page-return/page-return.jsx';
import { api_error_codes } from 'Constants/api-error-codes.js';
import { buy_sell } from 'Constants/buy-sell';
import { useStores } from 'Stores';
+import { ad_type } from 'Constants/floating-rate';
+import FloatingRate from 'Components/floating-rate';
+import EditAdCancelModal from 'Components/my-ads/edit-ad-cancel-modal.jsx';
import { generateErrorDialogTitle, generateErrorDialogBody } from 'Utils/adverts.js';
import EditAdFormPaymentMethods from './edit-ad-form-payment-methods.jsx';
import CreateAdAddPaymentMethodModal from './create-ad-add-payment-method-modal.jsx';
@@ -24,7 +27,7 @@ const EditAdFormWrapper = ({ children }) => {
};
const EditAdForm = () => {
- const { my_ads_store, my_profile_store } = useStores();
+ const { floating_rate_store, general_store, my_ads_store, my_profile_store } = useStores();
const available_balance = useUpdatingAvailableBalance();
const os = mobileOSDetect();
@@ -40,10 +43,24 @@ const EditAdForm = () => {
payment_method_details,
rate_display,
type,
+ is_active,
+ rate_type,
} = my_ads_store.p2p_advert_information;
const is_buy_advert = type === buy_sell.BUY;
const [selected_methods, setSelectedMethods] = React.useState([]);
+ const [is_cancel_edit_modal_open, setIsCancelEditModalOpen] = React.useState(false);
+ const [is_payment_method_touched, setIsPaymentMethodTouched] = React.useState(false);
+
+ const setInitialAdRate = () => {
+ if (my_ads_store.required_ad_type !== my_ads_store.selected_ad_type) {
+ if (my_ads_store.required_ad_type === ad_type.FLOAT) {
+ return is_buy_advert ? '-0.01' : '+0.01';
+ }
+ return '';
+ }
+ return rate_display;
+ };
const payment_methods_changed = is_buy_advert
? !(
@@ -60,6 +77,16 @@ const EditAdForm = () => {
selected_methods.length === Object.keys(payment_method_details).length
);
+ const handleEditAdFormCancel = is_form_edited => {
+ if (is_form_edited || payment_methods_changed) {
+ setIsCancelEditModalOpen(true);
+ } else {
+ my_ads_store.setShowEditAdForm(false);
+ }
+ };
+
+ const toggleEditAdCancelModal = is_cancel_edit =>
+ is_cancel_edit ? my_ads_store.setShowEditAdForm(false) : setIsCancelEditModalOpen(false);
const is_api_error = [api_error_codes.ADVERT_SAME_LIMITS, api_error_codes.DUPLICATE_ADVERT].includes(
my_ads_store.error_code
);
@@ -69,6 +96,9 @@ const EditAdForm = () => {
my_profile_store.getAdvertiserPaymentMethods();
my_ads_store.setIsEditAdErrorModalVisible(false);
my_ads_store.setEditAdFormError('');
+ floating_rate_store.setApiErrorMessage('');
+ // P2P configuration is not subscribed. Hence need to fetch it on demand
+ general_store.setP2PConfig();
if (payment_method_names && !payment_method_details) {
const selected_payment_method_values = [];
@@ -85,263 +115,347 @@ const EditAdForm = () => {
my_ads_store.payment_method_ids.push(pm[0]);
});
}
+ if (my_ads_store.required_ad_type !== rate_type) {
+ const is_payment_method_available =
+ !!Object.keys({ ...payment_method_details }).length ||
+ !!Object.values({ ...payment_method_names }).length;
+ setIsPaymentMethodTouched(is_payment_method_available);
+ }
return () => my_ads_store.setApiErrorCode(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
- my_ads_store.setShowEditAdForm(false)}
- page_title={localize('Edit {{ad_type}} ad', { ad_type: type })}
- />
-
- {({ dirty, errors, handleChange, isSubmitting, isValid, touched, values }) => {
- const is_sell_advert = values.type === buy_sell.SELL;
- 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 ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ my_ads_store.toggleMyAdsRateSwitchModal(
+ my_ads_store.selected_ad_type,
+ !floating_rate_store.reached_target_date
+ )
+ }
+ large
+ >
+
+
+ my_ads_store.toggleMyAdsRateSwitchModal(floating_rate_store.rate_type, true)}
+ >
+ {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}
+
+ setShouldExpandAll(prev_state => !prev_state)}
+ transparent
+ >
+
+ {localize('{{accordion_state}}', {
+ accordion_state: should_expand_all
+ ? 'Collapse all'
+ : 'Expand all',
+ })}
+
+
+
+
({
+ 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;