- {is_advert_active ? (
-
- ) : (
-
- )}
-
+ )}
+
+ {is_popover_actions_visible && (
+
+ {is_advert_active ? (
+
-
+ ) : (
+
+ )}
+
- )}
-
-
- >
+
+
+ )}
+
+
);
});
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 ab4640ac6aba..5aad566b28bf 100644
--- a/packages/p2p/src/components/my-ads/my-ads-table.jsx
+++ b/packages/p2p/src/components/my-ads/my-ads-table.jsx
@@ -24,8 +24,7 @@ const getHeaders = offered_currency => [
];
const MyAdsTable = () => {
- const { general_store, my_ads_store } = useStores();
-
+ const { floating_rate_store, general_store, my_ads_store } = useStores();
const [selected_advert, setSelectedAdvert] = React.useState(undefined);
const local_currency = general_store.client.local_currency_config.currency;
@@ -33,7 +32,7 @@ const MyAdsTable = () => {
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
}, []);
@@ -61,22 +60,31 @@ const MyAdsTable = () => {
is_warn
/>
)}
-
-
-
-
- }
- is_warn
- />
-
+ {floating_rate_store.change_ad_alert && (
+
+
+ {floating_rate_store.reached_target_date ? (
+
+ ) : (
+
+ )}
+
+ }
+ is_warn
+ />
+
+ )}
{isDesktop() && (
diff --git a/packages/p2p/src/components/my-ads/my-ads.scss b/packages/p2p/src/components/my-ads/my-ads.scss
index 579d7ac1e89d..0d773ccda6e6 100644
--- a/packages/p2p/src/components/my-ads/my-ads.scss
+++ b/packages/p2p/src/components/my-ads/my-ads.scss
@@ -6,7 +6,7 @@
@include mobile {
.page-return {
border-bottom: 2px solid var(--general-section-1);
- margin: unset;
+ margin: 1rem 0;
padding: 0 1.6rem 1.6rem;
}
@@ -190,6 +190,7 @@
position: fixed;
right: 0;
width: 100vw;
+ z-index: 4;
}
}
}
@@ -246,6 +247,11 @@
&-price {
color: var(--text-profit-success);
font-weight: bold;
+
+ .display-layout {
+ display: flex;
+ flex-direction: column;
+ }
}
&-popovers {
background-color: var(--general-main-1);
@@ -440,6 +446,7 @@
display: flex;
justify-content: unset;
width: unset;
+ gap: 1rem;
}
}
}
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/stores/floating-rate-store.js b/packages/p2p/src/stores/floating-rate-store.js
index da14d15290c7..125b07bae69d 100644
--- a/packages/p2p/src/stores/floating-rate-store.js
+++ b/packages/p2p/src/stores/floating-rate-store.js
@@ -1,6 +1,7 @@
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 { requestWS } from 'Utils/websocket';
export default class FloatingRateStore extends BaseStore {
@@ -11,6 +12,7 @@ export default class FloatingRateStore extends BaseStore {
@observable exchange_rate;
@observable change_ad_alert;
@observable api_error_message = '';
+
@computed
get rate_type() {
if (this.float_rate_adverts_status === 'enabled') {
@@ -19,6 +21,14 @@ export default class FloatingRateStore extends BaseStore {
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;
@@ -29,7 +39,7 @@ export default class FloatingRateStore extends BaseStore {
}
@action.bound
setFloatRateOffsetLimit(offset_limit) {
- this.float_rate_offset_limit = parseFloat(offset_limit);
+ this.float_rate_offset_limit = parseFloat(offset_limit).toFixed(2);
}
@action.bound
setFixedRateAdvertsEndDate(end_date) {
@@ -46,19 +56,21 @@ export default class FloatingRateStore extends BaseStore {
@action.bound
setExchangeRate(fiat_currency, local_currency) {
- const pay_load = {
+ const payload = {
exchange_rates: 1,
base_currency: fiat_currency,
subscribe: 1,
target_currency: local_currency,
};
- requestWS(pay_load).then(response => {
- if (!!response && response.error) {
- this.setApiErrorMessage(response.error.message);
- } else {
- const { rates } = response.exchange_rates;
- this.exchange_rate = parseFloat(rates[local_currency]);
- this.setApiErrorMessage(null);
+ requestWS(payload).then(response => {
+ if (response) {
+ if (response.error) {
+ this.setApiErrorMessage(response.error.message);
+ } else {
+ const { rates } = response.exchange_rates;
+ this.exchange_rate = parseFloat(rates[local_currency]);
+ this.setApiErrorMessage(null);
+ }
}
});
}
diff --git a/packages/p2p/src/stores/general-store.js b/packages/p2p/src/stores/general-store.js
index eb72d8551cc8..1cfef11f6676 100644
--- a/packages/p2p/src/stores/general-store.js
+++ b/packages/p2p/src/stores/general-store.js
@@ -389,11 +389,16 @@ export default class GeneralStore extends BaseStore {
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, order_payment_period } =
- response.website_status.p2p_config;
+ const {
+ fixed_rate_adverts,
+ float_rate_adverts,
+ float_rate_offset_limit,
+ fixed_rate_adverts_end_date,
+ order_payment_period,
+ } = response.website_status.p2p_config;
floating_rate_store.setFixedRateAdvertStatus(fixed_rate_adverts);
floating_rate_store.setFloatingRateAdvertStatus(float_rate_adverts);
- floating_rate_store.setFoatRateOffsetLimit(float_rate_offset_limit);
+ floating_rate_store.setFloatRateOffsetLimit(float_rate_offset_limit);
floating_rate_store.setFixedRateAdvertsEndDate(fixed_rate_adverts_end_date || null);
floating_rate_store.setApiErrorMessage(null);
this.setOrderTimeOut(order_payment_period);
diff --git a/packages/p2p/src/stores/index.js b/packages/p2p/src/stores/index.js
index 412444f9dada..805c441d10dd 100644
--- a/packages/p2p/src/stores/index.js
+++ b/packages/p2p/src/stores/index.js
@@ -2,18 +2,19 @@ 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';
import OrderDetailsStore from './order-details-store';
import SendbirdStore from './sendbird-store';
-import FloatingRateStore from './floating-rate-store';
class RootStore {
constructor() {
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);
diff --git a/packages/p2p/src/stores/my-ads-store.js b/packages/p2p/src/stores/my-ads-store.js
index 0cb904e21848..0a4cc5a381d1 100644
--- a/packages/p2p/src/stores/my-ads-store.js
+++ b/packages/p2p/src/stores/my-ads-store.js
@@ -2,9 +2,10 @@ import { action, 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 {
@@ -33,14 +34,16 @@ 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 should_switch_ad_rate = false;
@observable update_payment_methods_error_message = '';
payment_method_ids = [];
@@ -133,7 +136,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 }
: {}),
@@ -252,7 +256,7 @@ export default class MyAdsStore extends BaseStore {
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: Number(values.rate_type),
...(this.payment_method_names.length > 0 && !is_sell_ad
? { payment_method_names: this.payment_method_names }
: {}),
@@ -311,20 +315,28 @@ 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));
+ 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);
} else {
@@ -338,10 +350,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;
@@ -349,6 +360,18 @@ export default class MyAdsStore extends BaseStore {
handleChange(e);
};
+ @action.bound
+ restrictDecimalPlace = (e, decimal_place, handleChangeCallback) => {
+ const pattern = new RegExp(`^[+-]?\\d*(\\.)?(\\d{1,${decimal_place}})?$`);
+ if (e.target.value.length > 7) {
+ e.target.value = e.target.value.slice(0, 7);
+ return;
+ }
+ if (pattern.test(e.target.value)) {
+ handleChangeCallback(e);
+ }
+ };
+
@action.bound
showQuickAddModal(advert) {
this.setSelectedAdId(advert);
@@ -525,6 +548,22 @@ export default class MyAdsStore extends BaseStore {
this.show_edit_ad_form = show_edit_ad_form;
}
+ @action.bound
+ setIsSwitchModalOpen(is_switch_modal_open, ad_id) {
+ this.setSelectedAdId(ad_id);
+ this.is_switch_modal_open = is_switch_modal_open;
+ }
+
+ @action.bound
+ setShouldSwitchAdRateStatus(should_switch_ad_rate) {
+ this.should_switch_ad_rate = should_switch_ad_rate;
+ if (should_switch_ad_rate) {
+ this.setShowEditAdForm(true);
+ this.getAdvertInfo();
+ }
+ this.setIsSwitchModalOpen(false, null);
+ }
+
@action.bound
setUpdatePaymentMethodsErrorMessage(update_payment_methods_error_message) {
this.update_payment_methods_error_message = update_payment_methods_error_message;
@@ -532,6 +571,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: [
@@ -540,7 +580,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),
],
@@ -550,7 +590,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),
],
@@ -561,17 +601,24 @@ 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) <=
+ this.root_store.general_store.client.local_currency_config.decimal_places
+ : true,
+ v =>
+ floating_rate_store.rate_type === ad_type.FLOAT
+ ? rangeValidator(parseFloat(v), this.root_store.floating_rate_store.float_rate_offset_limit)
+ : true,
],
};
@@ -585,7 +632,11 @@ 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:
+ this.root_store.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 })];
@@ -636,6 +687,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: this.root_store.floating_rate_store.float_rate_offset_limit,
+ }),
];
const errors = {};
@@ -659,7 +713,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:
@@ -713,7 +767,7 @@ export default class MyAdsStore extends BaseStore {
// 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 =>
@@ -733,7 +787,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: localize('Fixed rate'),
};
const getCommonMessages = field_name => [localize('{{field_name}} is required', { field_name })];
@@ -807,7 +861,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:
diff --git a/packages/p2p/src/utils/format-value.js b/packages/p2p/src/utils/format-value.js
new file mode 100644
index 000000000000..5ffb5dbc343e
--- /dev/null
+++ b/packages/p2p/src/utils/format-value.js
@@ -0,0 +1,13 @@
+export const roundOffDecimal = (number, decimal_place = 2) => {
+ // Rounds of the digit to the specified decimal place
+ 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;
+};
diff --git a/packages/p2p/src/utils/validations.js b/packages/p2p/src/utils/validations.js
index 44f197422a29..180c75fd759d 100644
--- a/packages/p2p/src/utils/validations.js
+++ b/packages/p2p/src/utils/validations.js
@@ -4,5 +4,8 @@ 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 5a04aa61a43b..12bb2d689eac 100644
--- a/packages/shared/src/styles/themes.scss
+++ b/packages/shared/src/styles/themes.scss
@@ -89,6 +89,7 @@
--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};
@@ -162,6 +163,7 @@
// Transparentize
--transparent-success: #{$alpha-color-green-1};
--transparent-info: #{$alpha-color-blue-1};
+ --transparent-hint: #{$alpha-color-blue-3};
/* TODO: change to styleguide later */
// Gradient
--gradient-success: #{$gradient-color-green-1};
@@ -198,6 +200,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};
@@ -247,7 +251,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 +259,7 @@
// Transparentize
--transparent-success: #{$alpha-color-green-2};
--transparent-info: #{$alpha-color-blue-1};
+ --transparent-hint: #{$alpha-color-blue-1};
/* TODO: change to styleguide later */
// Gradient
--gradient-success: #{$gradient-color-green-2};