diff --git a/src/components/layout/header/header.tsx b/src/components/layout/header/header.tsx index 02bf74857..1ff312658 100644 --- a/src/components/layout/header/header.tsx +++ b/src/components/layout/header/header.tsx @@ -12,7 +12,6 @@ import { requestOidcAuthentication } from '@deriv-com/auth-client'; import { Localize, useTranslations } from '@deriv-com/translations'; import { Header, useDevice, Wrapper } from '@deriv-com/ui'; import { Tooltip } from '@deriv-com/ui'; -import { isDotComSite } from '../../../utils'; import { AppLogo } from '../app-logo'; import AccountsInfoLoader from './account-info-loader'; import AccountSwitcher from './account-switcher'; @@ -122,21 +121,19 @@ const AppHeader = observer(() => { const query_param_currency = sessionStorage.getItem('query_param_currency') || currency || 'USD'; try { - if (isDotComSite()) { - await requestOidcAuthentication({ - redirectCallbackUri: `${window.location.origin}/callback`, - ...(query_param_currency - ? { - state: { - account: query_param_currency, - }, - } - : {}), - }).catch(err => { - // eslint-disable-next-line no-console - console.error(err); - }); - } + await requestOidcAuthentication({ + redirectCallbackUri: `${window.location.origin}/callback`, + ...(query_param_currency + ? { + state: { + account: query_param_currency, + }, + } + : {}), + }).catch(err => { + // eslint-disable-next-line no-console + console.error(err); + }); } catch (error) { // eslint-disable-next-line no-console console.error(error); diff --git a/src/components/layout/index.tsx b/src/components/layout/index.tsx index 3b59ae319..086b5ba1f 100644 --- a/src/components/layout/index.tsx +++ b/src/components/layout/index.tsx @@ -5,7 +5,6 @@ import { Outlet } from 'react-router-dom'; import { api_base } from '@/external/bot-skeleton'; import { requestOidcAuthentication } from '@deriv-com/auth-client'; import { useDevice } from '@deriv-com/ui'; -import { isDotComSite } from '../../utils'; import { crypto_currencies_display_order, fiat_currencies_display_order } from '../shared'; import Footer from './footer'; import AppHeader from './header'; @@ -124,7 +123,7 @@ const Layout = () => { const checkOIDCEnabledWithMissingAccount = !isEndpointPage && !isCallbackPage && !clientHasCurrency; if ( - (isDotComSite() && isLoggedInCookie && !isClientAccountsPopulated && !isEndpointPage && !isCallbackPage) || + (isLoggedInCookie && !isClientAccountsPopulated && !isEndpointPage && !isCallbackPage) || checkOIDCEnabledWithMissingAccount ) { const query_param_currency = sessionStorage.getItem('query_param_currency') || currency || 'USD'; diff --git a/src/components/shared_ui/input/input.tsx b/src/components/shared_ui/input/input.tsx index 0f17175ac..efa271109 100644 --- a/src/components/shared_ui/input/input.tsx +++ b/src/components/shared_ui/input/input.tsx @@ -94,6 +94,17 @@ const Input = React.forwardRef { const [counter, setCounter] = React.useState(0); + const [, setIsDropdownOpen] = React.useState(false); + + const handleFocus = (e: React.FocusEvent) => { + setIsDropdownOpen(true); + props.onFocus?.(e); + }; + + const handleBlur = (e: React.FocusEvent) => { + setIsDropdownOpen(false); + props.onBlur?.(e); + }; React.useEffect(() => { if (initial_character_count || initial_character_count === 0) { @@ -102,26 +113,8 @@ const Input = React.forwardRef = e => { - let input_value = e.target.value; + const input_value = e.target.value; - // Special handling for tick_count - completely prevent decimal input - if (e.target.name === 'tick_count') { - // Remove any decimal point and everything after it - const decimal_index = input_value.indexOf('.'); - if (decimal_index !== -1) { - input_value = input_value.substring(0, decimal_index); - } - } - // For other inputs, limit to 2 decimal places - else { - const decimal_index = input_value.indexOf('.'); - if (decimal_index !== -1) { - const decimal_part = input_value.substring(decimal_index + 1); - if (decimal_part.length > 2) { - input_value = input_value.substring(0, decimal_index + 3); - } - } - } setCounter(input_value.length); e.target.value = input_value; props.onChange?.(e); @@ -171,8 +164,8 @@ const Input = React.forwardRef { + contentLocation: ({ + childRect, + popoverRect, + nudgedLeft, + }: { + childRect: DOMRect; + popoverRect: DOMRect; + nudgedLeft: number; + }) => { const screen_width = document.body.clientWidth; const total_width = childRect.right + (popoverRect.width - childRect.width / 2); let top_offset = 0; @@ -155,8 +163,87 @@ const Popover = ({ }, } : { containerStyle: { zIndex } })} - content={({ position, childRect, popoverRect }) => { - return ( + content={(popoverState: PopoverState): JSX.Element => { + const { position, childRect, popoverRect } = popoverState; + const modalContent = document.querySelector('.qs__body__content'); + const modalHeader = document.querySelector('.modal__header'); + const modalFooter = document.querySelector('.modal__footer'); + + const contentRect = modalContent?.getBoundingClientRect(); + const headerRect = modalHeader?.getBoundingClientRect(); + const footerRect = modalFooter?.getBoundingClientRect(); + + // Add safety margins + const safetyMargin = 8; + // Initialize offsets + let offsetTop = 0; + let offsetLeft = 0; + + // Only proceed if we have all required elements + if (contentRect && childRect) { + // Define safe zone between header and footer + const safeTop = headerRect ? headerRect.bottom + safetyMargin : contentRect.top; + const safeBottom = footerRect ? footerRect.top - safetyMargin : contentRect.bottom; + + // Check if input is outside safe zone + const isOutsideSafeZone = + childRect.bottom < safeTop || + childRect.top > safeBottom || + childRect.right < contentRect.left + safetyMargin || + childRect.left > contentRect.right - safetyMargin; + + if (isOutsideSafeZone) { + return
; + } + + // If popover exists, adjust its position if needed + if (popoverRect) { + // Adjust vertical position + if (popoverRect.top < safeTop) { + offsetTop = safeTop - popoverRect.top; + } else if (popoverRect.bottom > safeBottom) { + offsetTop = safeBottom - popoverRect.bottom; + } + + // Adjust horizontal position + if (popoverRect.left < contentRect.left + safetyMargin) { + offsetLeft = contentRect.left + safetyMargin - popoverRect.left; + } else if (popoverRect.right > contentRect.right - safetyMargin) { + offsetLeft = contentRect.right - safetyMargin - popoverRect.right; + } + } + } + + // Create the popover content + const PopoverContent = () => ( +
void} + > + {!disable_message_icon && icon === 'info' && ( + + + + )} + {(has_error && ( + + {message} + + )) || ( + + {message} + + )} +
+ ); + + // Create the arrow container with content + const ArrowContainerWithContent = () => ( -
void} - > - {!disable_message_icon && icon === 'info' && ( - - - - )} - {(has_error && ( - - {message} - - )) || ( - - {message} - - )} -
+
); + + // Return with or without transform wrapper + return offsetTop !== 0 || offsetLeft !== 0 ? ( +
+ +
+ ) : ( + + ); }} >
diff --git a/src/hooks/auth/useOauth2.ts b/src/hooks/auth/useOauth2.ts index c0aeeb763..8367f6a10 100644 --- a/src/hooks/auth/useOauth2.ts +++ b/src/hooks/auth/useOauth2.ts @@ -3,7 +3,6 @@ import { useEffect } from 'react'; import Cookies from 'js-cookie'; import RootStore from '@/stores/root-store'; import { OAuth2Logout, requestOidcAuthentication } from '@deriv-com/auth-client'; -import { isDotComSite } from '../../utils'; /** * Provides an object with properties: `oAuthLogout`, `retriggerOAuth2Login`, and `isSingleLoggingIn`. @@ -73,15 +72,13 @@ export const useOauth2 = ({ }; const retriggerOAuth2Login = async () => { try { - if (isDotComSite()) { - await requestOidcAuthentication({ - redirectCallbackUri: `${window.location.origin}/callback`, - postLogoutRedirectUri: window.location.origin, - }).catch(err => { - // eslint-disable-next-line no-console - console.error('Error during OAuth2 login retrigger:', err); - }); - } + await requestOidcAuthentication({ + redirectCallbackUri: `${window.location.origin}/callback`, + postLogoutRedirectUri: window.location.origin, + }).catch(err => { + // eslint-disable-next-line no-console + console.error('Error during OAuth2 login retrigger:', err); + }); } catch (error) { // eslint-disable-next-line no-console console.error('Error during OAuth2 login retrigger:', error); diff --git a/src/pages/bot-builder/quick-strategy/config.ts b/src/pages/bot-builder/quick-strategy/config.ts index d6832372f..ef0a3e27d 100644 --- a/src/pages/bot-builder/quick-strategy/config.ts +++ b/src/pages/bot-builder/quick-strategy/config.ts @@ -75,7 +75,7 @@ const TAKE_PROFIT = (): TConfigItem => ({ 'ceil', { type: 'min', - value: 0.35, + value: 1, getMessage: (min: string | number) => localize('Minimum take profit allowed is {{ min }}', { min }), }, ], @@ -156,9 +156,9 @@ const STAKE = (): TConfigItem => ({ 'ceil', { type: 'min', - value: 0.35, + value: 1, getMessage: (min: string | number) => localize('Minimum stake allowed is {{ min }}', { min }), - getDynamicValue: (store: any) => store.quick_strategy?.additional_data?.min_stake || 0.35, + getDynamicValue: (store: any) => store.quick_strategy?.additional_data?.min_stake || 1, }, { type: 'max', diff --git a/src/pages/bot-builder/quick-strategy/form-wrappers/strategy-template-picker.scss b/src/pages/bot-builder/quick-strategy/form-wrappers/strategy-template-picker.scss index 9c815264f..64940e993 100644 --- a/src/pages/bot-builder/quick-strategy/form-wrappers/strategy-template-picker.scss +++ b/src/pages/bot-builder/quick-strategy/form-wrappers/strategy-template-picker.scss @@ -47,6 +47,12 @@ &__wrapper__variant--outline { background-color: unset; border: none; + border: 1px solid var(--general-section-6); + padding-right: 0; + } + + &__wrapper--has-value.quill-input__wrapper__variant--outline { + padding-right: 0.8rem !important; } } } diff --git a/src/pages/bot-builder/quick-strategy/inputs/qs-input.tsx b/src/pages/bot-builder/quick-strategy/inputs/qs-input.tsx index c28f6e8b7..9de238726 100644 --- a/src/pages/bot-builder/quick-strategy/inputs/qs-input.tsx +++ b/src/pages/bot-builder/quick-strategy/inputs/qs-input.tsx @@ -6,6 +6,25 @@ import Input from '@/components/shared_ui/input'; import Popover from '@/components/shared_ui/popover'; import { useStore } from '@/hooks/useStore'; +// Helper function to format numbers based on currency +const formatNumberForCurrency = (value: number, currency?: string): string => { + // Format the number based on the currency + switch (currency) { + case 'BTC': + // For BTC, show 8 decimal places + return value.toFixed(8); + case 'ETH': + // For ETH, show 6 decimal places + return value.toFixed(6); + case 'LTC': + // For LTC, show 8 decimal places + return value.toFixed(8); + default: + // For other currencies (USD, tUSDT, eUSDT), show 2 decimal places + return value.toFixed(2); + } +}; + type TQSInput = { name: string; onChange: (key: string, value: string | number | boolean) => void; @@ -61,24 +80,99 @@ const QSInput: React.FC = observer( client: { currency }, } = useStore(); const { quick_strategy } = useStore(); - const { loss_threshold_warning_data } = quick_strategy; - + const { loss_threshold_warning_data, is_dropdown_open: store_dropdown_open } = quick_strategy; + const popoverRef = React.useRef(null); const [, setFocus] = React.useState(false); const [error_message, setErrorMessage] = React.useState(null); + const [local_dropdown_open, setLocalDropdownOpen] = React.useState(false); const { setFieldValue, setFieldTouched, values } = useFormikContext<{ stake?: string | number; max_stake?: string | number; }>(); + + // Use either the store's dropdown state or our local state + const is_dropdown_open = store_dropdown_open || local_dropdown_open; + + // Set up a MutationObserver to detect when the dropdown opens/closes + useEffect(() => { + // Function to check if dropdown is open + const checkDropdownState = () => { + // Look for elements that might indicate an open dropdown + const openDropdowns = document.querySelectorAll( + '.dropdown-open, [data-open="true"], [aria-expanded="true"]' + ); + const dropdownMenus = document.querySelectorAll('.dropdown-menu, .select-dropdown, .menu-open'); + + // Check if there's a dropdown with "Continuous Indices" text (from the screenshot) + const continuousIndicesElements = Array.from(document.querySelectorAll('*')).filter(el => + el.textContent?.includes('Continuous Indices') + ); + + // If any of these elements exist, consider the dropdown open + const isOpen = + openDropdowns.length > 0 || dropdownMenus.length > 0 || continuousIndicesElements.length > 0; + + setLocalDropdownOpen(isOpen); + }; + + // Check initial state + checkDropdownState(); + + // Set up observer to watch for DOM changes + const observer = new MutationObserver(checkDropdownState); + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['class', 'style', 'aria-expanded', 'data-open'], + }); + + // Clean up + return () => observer.disconnect(); + }, []); const is_number = type === 'number'; const max_value = 999999999999; + // Add useEffect to add a global style to hide all popovers when dropdown is open + useEffect(() => { + // Create a style element + const styleElement = document.createElement('style'); + styleElement.id = 'hide-popovers-style'; + + // Add CSS to hide all popovers when dropdown is open + if (is_dropdown_open) { + styleElement.textContent = ` + .qs__warning-bubble { + display: none !important; + opacity: 0 !important; + visibility: hidden !important; + } + `; + document.head.appendChild(styleElement); + } else { + // Remove the style element when dropdown is closed + const existingStyle = document.getElementById('hide-popovers-style'); + if (existingStyle) { + document.head.removeChild(existingStyle); + } + } + + // Cleanup function to remove the style element when component unmounts + return () => { + const existingStyle = document.getElementById('hide-popovers-style'); + if (existingStyle) { + document.head.removeChild(existingStyle); + } + }; + }, [is_dropdown_open]); + // Add useEffect to watch for changes in stake values and update error message accordingly useEffect(() => { // For max_stake field: show error if initial stake > max stake if (name === 'max_stake' && values.stake && values.max_stake) { - // Convert to numbers with fixed precision to handle floating point comparison correctly - const initial_stake = parseFloat(Number(values.stake).toFixed(2)); - const max_stake = parseFloat(Number(values.max_stake).toFixed(2)); + // Convert to numbers without limiting decimal places + const initial_stake = parseFloat(String(values.stake)); + const max_stake = parseFloat(String(values.max_stake)); if (initial_stake > max_stake) { // If initial stake is greater than max stake, show error @@ -90,12 +184,14 @@ const QSInput: React.FC = observer( // Add error class to the input max_stake_input.closest('.qs__input')?.classList.add('error'); - // Force the popover to show + // Force the popover to show only if dropdown is not open const popover = max_stake_input .closest('.qs__form__field__input') ?.querySelector('.qs__warning-bubble'); - if (popover) { + if (popover && !is_dropdown_open) { popover.setAttribute('data-show', 'true'); + } else if (popover) { + popover.removeAttribute('data-show'); } } } else { @@ -133,15 +229,22 @@ const QSInput: React.FC = observer( messageElement.textContent = 'Initial stake cannot be greater than max stake'; } - // Try to force the popover to be visible by directly manipulating the DOM + // Try to force the popover to be visible by directly manipulating the DOM, but only if dropdown is not open const popoverElement = max_stake_component.querySelector('.qs__warning-bubble'); if (popoverElement) { - // Make the popover visible - (popoverElement as HTMLElement).style.display = 'block'; - (popoverElement as HTMLElement).style.opacity = '1'; - (popoverElement as HTMLElement).style.visibility = 'visible'; - (popoverElement as HTMLElement).style.position = 'absolute'; - (popoverElement as HTMLElement).style.zIndex = '9999'; + if (!is_dropdown_open) { + // Make the popover visible + (popoverElement as HTMLElement).style.display = 'block'; + (popoverElement as HTMLElement).style.opacity = '1'; + (popoverElement as HTMLElement).style.visibility = 'visible'; + (popoverElement as HTMLElement).style.position = 'absolute'; + (popoverElement as HTMLElement).style.zIndex = '9999'; + } else { + // Hide the popover when dropdown is open + (popoverElement as HTMLElement).style.display = 'none'; + (popoverElement as HTMLElement).style.opacity = '0'; + (popoverElement as HTMLElement).style.visibility = 'hidden'; + } } } @@ -176,10 +279,10 @@ const QSInput: React.FC = observer( } } } - }, [name, values.stake, values.max_stake]); + }, [name, values.stake, values.max_stake, is_dropdown_open]); const handleButtonInputChange = (e: MouseEvent, value: string) => { - e?.preventDefault(); + e.preventDefault(); // Changed from e?.preventDefault() to ensure it always runs // For tick_count field or duration field with ticks, ensure we only allow integer values if (name === 'tick_count' || (name === 'duration' && quick_strategy.form_data?.durationtype === 't')) { @@ -187,9 +290,9 @@ const QSInput: React.FC = observer( value = String(intValue); } - // For stake field, ensure the value is within the allowed range - if (name === 'stake') { - const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 0.35; + // For stake and max_stake fields, ensure the value is within the allowed range + if (name === 'stake' || name === 'max_stake') { + const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 1; const max_stake = (quick_strategy?.additional_data as any)?.max_stake || 1000; if (Number(value) < min_stake) { @@ -199,37 +302,77 @@ const QSInput: React.FC = observer( } } - onChange(name, value); - setFieldTouched(name, true, true); - setFieldValue(name, value); + // For Accumulators trade type, we need to ensure the value is properly updated + // in both Formik state and the store + const is_accumulator = quick_strategy.selected_strategy.includes('ACCUMULATORS'); + + // Note: We're not using window.userStakeValue anymore as it's not a proper approach + // The userStakeValue ref in growth-rate-type.tsx will be updated when the form values change + + if (is_accumulator) { + // For Accumulators, update the store first, then Formik state + onChange(name, value); + setFieldTouched(name, true, true); + + // Use a small timeout to ensure the value is updated in the DOM + // This helps with synchronization issues between Formik and the store + setTimeout(() => { + setFieldValue(name, value); + }, 0); + } else { + // For Options, update Formik state first, then the store + setFieldValue(name, value); + setFieldTouched(name, true, true); + onChange(name, value); + } }; const handleOnChange = (e: React.ChangeEvent) => { const input_value = e.target.value; let value: number | string = 0; - const max_characters = 12; + + // Don't restrict the number of characters + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const max_characters = undefined; // Clear any previous error message setErrorMessage(null); - // Allow empty string or partial input to support backspace - if (input_value === '' || input_value === '0' || input_value === '0.') { - onChange(name, input_value); + // Check if the input is effectively zero (empty, '0', '0.', '0.00', etc.) + const is_effectively_zero = input_value === '' || input_value === '0' || /^0\.0*$/.test(input_value); - // Show error message for empty values in fields + // Allow empty string or partial input to support backspace + if (is_effectively_zero) { + // Show error message for effectively zero values in fields if (name === 'stake' || name === 'max_stake') { - const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 0.35; + const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 1; setErrorMessage(`Minimum stake allowed is ${min_stake}`); } + + // For Accumulators trade type, we need to ensure the value is properly updated + // in both Formik state and the store + const is_accumulator = quick_strategy.selected_strategy.includes('ACCUMULATORS'); + + if (is_accumulator) { + // For Accumulators, update the store first, then Formik state + onChange(name, input_value); + setFieldTouched(name, true, true); + + // Use a small timeout to ensure the value is updated in the DOM + setTimeout(() => { + setFieldValue(name, input_value); + }, 0); + } else { + // For Options, update Formik state first, then the store + setFieldValue(name, input_value); + setFieldTouched(name, true, true); + onChange(name, input_value); + } return; } - if (max_characters && input_value.length >= max_characters) { - value = input_value.slice(0, max_characters); - value = is_number ? Number(value) : value; - } else { - value = is_number ? Number(input_value) : input_value; - } + // Don't restrict the input value length + value = is_number ? parseFloat(input_value) : input_value; // For tick_count field or duration field with ticks, ensure we only allow integer values if ( @@ -240,14 +383,9 @@ const QSInput: React.FC = observer( value = Math.floor(Number(value)); } - // For all number fields, prevent decimal values less than 1 - if (is_number && typeof value === 'number' && value < 1 && !Number.isInteger(value)) { - value = 1; - } - // For stake field, check if value is within the allowed range if (name === 'stake' && is_number) { - const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 0.35; + const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 1; const max_stake = (quick_strategy?.additional_data as any)?.max_stake || 1000; const numValue = Number(value); @@ -259,18 +397,19 @@ const QSInput: React.FC = observer( if (numValue < min_stake) { setErrorMessage(`Minimum stake allowed is ${min_stake}`); } else if (numValue > max_stake) { + console.log('test max_stake', max_stake); // Allow entering any value but show error message setErrorMessage(`Maximum stake allowed is ${max_stake}`); } } - ``; // Note: We're not adding a custom error message for when initial stake > max stake + // Note: We're not adding a custom error message for when initial stake > max stake // The standard error message will be displayed by the form validation } // For max_stake field, check if value is within the allowed range if (name === 'max_stake' && is_number) { - const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 0.35; + const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 1; const max_stake = (quick_strategy?.additional_data as any)?.max_stake || 1000; const numValue = Number(value); @@ -282,6 +421,7 @@ const QSInput: React.FC = observer( if (numValue < min_stake) { setErrorMessage(`Minimum stake allowed is ${min_stake}`); } else if (numValue > max_stake) { + console.log('test max_stake', max_stake); // Allow entering any value but show error message setErrorMessage(`Maximum stake allowed is ${max_stake}`); } @@ -295,7 +435,25 @@ const QSInput: React.FC = observer( } } - onChange(name, value); + // For Accumulators trade type, we need to ensure the value is properly updated + // in both Formik state and the store + const is_accumulator = quick_strategy.selected_strategy.includes('ACCUMULATORS'); + + if (is_accumulator) { + // For Accumulators, update the store first, then Formik state + onChange(name, value); + setFieldTouched(name, true, true); + + // Use a small timeout to ensure the value is updated in the DOM + setTimeout(() => { + setFieldValue(name, value); + }, 0); + } else { + // For Options, update Formik state first, then the store + setFieldValue(name, value); + setFieldTouched(name, true, true); + onChange(name, value); + } }; return ( @@ -304,6 +462,23 @@ const QSInput: React.FC = observer( const { error } = meta; const has_error = error; const is_exclusive_field = has_currency_unit; + + // Add useEffect to update error message when additional_data changes + useEffect(() => { + // If the field is effectively zero and it's a stake field, show the minimum stake error + if (name === 'stake' || name === 'max_stake') { + // Check if the input is effectively zero (empty, '0', '0.', '0.00', etc.) + const is_effectively_zero = + !field.value || + field.value === '0' || + (typeof field.value === 'string' && /^0\.0*$/.test(field.value)); + + if (is_effectively_zero) { + const min_stake = (quick_strategy?.additional_data as any)?.min_stake || 1; + setErrorMessage(`Minimum stake allowed is ${min_stake}`); + } + } + }, [quick_strategy?.additional_data, field.value]); return (
= observer( onMouseEnter={() => setFocus(true)} onMouseLeave={() => setFocus(false)} > - + <> = observer( disabled={ disabled || (!!min && Number(field.value) === min) || - (name === 'stake' && + ((name === 'stake' || name === 'max_stake') && Number(field.value) <= ((quick_strategy?.additional_data as any)?.min_stake || - 0.35)) || - Number(field.value) <= 1 // Disable minus button for all inputs when value is <= 1 + 1)) || + (name !== 'stake' && + name !== 'max_stake' && + Number(field.value) <= 1) // Only disable minus button for non-stake inputs when value is <= 1 } data-testid='qs-input-decrease' onClick={(e: MouseEvent) => { + e.preventDefault(); // Explicitly prevent default button behavior const min_stake = - (quick_strategy?.additional_data as any)?.min_stake || 0.35; + (quick_strategy?.additional_data as any)?.min_stake || 1; + const increment_step = + (quick_strategy?.additional_data as any)?.increment_step || + 1; const current_value = Number(field.value); - const field_min = name === 'stake' ? min_stake : min || 1; - - // For all fields - // If value is greater than 1, subtract 1 from the value - if (current_value > 1) { - const new_value = current_value - 1; - handleButtonInputChange( - e, - String(new_value % 1 ? new_value.toFixed(2) : new_value) - ); - return; + const field_min = + name === 'stake' || name === 'max_stake' + ? min_stake + : min || 1; + + // For stake and max_stake fields + if (name === 'stake' || name === 'max_stake') { + // If value is greater than minimum + increment_step, subtract increment_step + if (current_value > field_min + increment_step) { + const new_value = current_value - increment_step; + // Format the number based on the currency + const formatted_value = formatNumberForCurrency( + new_value, + currency + ); + handleButtonInputChange(e, formatted_value); + return; + } + // If value is between minimum and minimum + increment_step, set to minimum + else if ( + current_value > field_min && + current_value <= field_min + increment_step + ) { + const formatted_value = formatNumberForCurrency( + field_min, + currency + ); + handleButtonInputChange(e, formatted_value); + return; + } + // If already at minimum, do nothing + else if (current_value <= field_min) { + return; + } } - // If value is less than or equal to 1 but greater than minimum, set to minimum - else if (current_value <= 1 && current_value > field_min) { - handleButtonInputChange(e, String(field_min)); - return; + // For profit, loss, and take_profit fields + else if ( + name === 'profit' || + name === 'loss' || + name === 'take_profit' + ) { + // Get the increment step from additional_data + const increment_step = + (quick_strategy?.additional_data as any) + ?.increment_step || 1; + + // If value is greater than minimum + increment_step, subtract increment_step + if (current_value > field_min + increment_step) { + const new_value = current_value - increment_step; + // Format the number based on the currency + const formatted_value = formatNumberForCurrency( + new_value, + currency + ); + handleButtonInputChange(e, formatted_value); + return; + } + // If value is between minimum and minimum + increment_step, set to minimum + else if ( + current_value > field_min && + current_value <= field_min + increment_step + ) { + const formatted_value = formatNumberForCurrency( + field_min, + currency + ); + handleButtonInputChange(e, formatted_value); + return; + } + // If already at minimum, do nothing + else if (current_value <= field_min) { + return; + } } - // If already at minimum, do nothing - else if (current_value <= field_min) { - return; + // For all other fields + else { + // If value is greater than 1, subtract 1 from the value + if (current_value > 1) { + const new_value = current_value - 1; + handleButtonInputChange( + e, + String( + new_value % 1 ? new_value.toFixed(2) : new_value + ) + ); + return; + } + // If value is less than or equal to 1 but greater than minimum, set to minimum + else if (current_value <= 1 && current_value > field_min) { + handleButtonInputChange(e, String(field_min)); + return; + } + // If already at minimum, do nothing + else if (current_value <= field_min) { + return; + } } }} > @@ -406,11 +645,39 @@ const QSInput: React.FC = observer( } data-testid='qs-input-increase' onClick={(e: MouseEvent) => { - const value = Number(field.value) + 1; - handleButtonInputChange( - e, - String(value % 1 ? value.toFixed(2) : value) - ); + e.preventDefault(); // Explicitly prevent default button behavior + // Get the increment step from additional_data + const increment_step = + name === 'stake' || + name === 'max_stake' || + name === 'profit' || + name === 'loss' || + name === 'take_profit' + ? (quick_strategy?.additional_data as any) + ?.increment_step || 1 + : 1; + + const value = Number(field.value) + increment_step; + + // Format the value based on the currency if it's a stake, max_stake, profit, loss, or take_profit field + if ( + name === 'stake' || + name === 'max_stake' || + name === 'profit' || + name === 'loss' || + name === 'take_profit' + ) { + const formatted_value = formatNumberForCurrency( + value, + currency + ); + handleButtonInputChange(e, formatted_value); + } else { + handleButtonInputChange( + e, + String(value % 1 ? value.toFixed(2) : value) + ); + } }} > + @@ -424,27 +691,32 @@ const QSInput: React.FC = observer( // Don't reset empty values, just show error message if (name === 'stake' || name === 'max_stake') { const min_stake = - (quick_strategy?.additional_data as any)?.min_stake || 0.35; + (quick_strategy?.additional_data as any)?.min_stake || 1; const max_stake = (quick_strategy?.additional_data as any)?.max_stake || 1000; const value = e.target.value; - // For empty values, show error message but don't reset - if (value === '' || value === '0' || value === '0.') { + // Check if the input is effectively zero (empty, '0', '0.', '0.00', etc.) + const is_effectively_zero = + value === '' || value === '0' || /^0\.0*$/.test(value); + + // For effectively zero values, show error message but don't reset + if (is_effectively_zero) { setErrorMessage(`Minimum stake allowed is ${min_stake}`); } else { // For non-empty values, validate and show appropriate message const numValue = Number(value); - // Prevent decimal values less than 1 - if (numValue < 1 && !Number.isInteger(numValue)) { - setFieldValue(name, 1); - return; - } + // // Prevent decimal values less than 1 + // if (numValue < 1 && !Number.isInteger(numValue)) { + // setFieldValue(name, 1); + // return; + // } if (numValue < min_stake) { setErrorMessage(`Minimum stake allowed is ${min_stake}`); } else if (numValue > max_stake) { + console.log('test max_stake', max_stake); setErrorMessage(`Maximum stake allowed is ${max_stake}`); } else { setErrorMessage(null); @@ -514,23 +786,19 @@ const QSInput: React.FC = observer( }} placeholder={is_exclusive_field ? '0.00' : ''} bottom_label={is_exclusive_field ? currency : ''} - max_characters={2} - maxLength={2} - inputMode={ - name === 'tick_count' || - (name === 'duration' && quick_strategy.form_data?.durationtype === 't') - ? 'numeric' - : undefined - } + // Remove restrictions on number of characters + max_characters={undefined} + maxLength={undefined} pattern={ name === 'tick_count' || (name === 'duration' && quick_strategy.form_data?.durationtype === 't') ? '[0-9]*' : undefined } + step='any' // Allow any decimal precision for all currencies onKeyPress={ name === 'tick_count' || - (name === 'duration' && quick_strategy.form_data?.durationtype === 't') + (name === 'duration' && quick_strategy.form_data?.durationtype === 's') ? e => { if (e.key === '.') { e.preventDefault(); @@ -538,28 +806,47 @@ const QSInput: React.FC = observer( } : undefined } + // Remove any restrictions on decimal input for cryptocurrency fields + inputMode={ + (name === 'stake' || + name === 'max_stake' || + name === 'profit' || + name === 'loss' || + name === 'take_profit') && + (currency === 'BTC' || currency === 'ETH' || currency === 'LTC') + ? 'decimal' + : name === 'tick_count' || + (name === 'duration' && + quick_strategy.form_data?.durationtype === 't') + ? 'numeric' + : undefined + } onKeyUp={e => { // Check value on each keystroke for stake field if (name === 'stake' || name === 'max_stake') { const min_stake = - (quick_strategy?.additional_data as any)?.min_stake || 0.35; + (quick_strategy?.additional_data as any)?.min_stake || 1; const max_stake = (quick_strategy?.additional_data as any)?.max_stake || 1000; const value = e.currentTarget.value; - // For empty values, show error message but don't reset - if (value === '' || value === '0' || value === '0.') { + // Check if the input is effectively zero (empty, '0', '0.', '0.00', etc.) + const is_effectively_zero = + value === '' || value === '0' || /^0\.0*$/.test(value); + + // For effectively zero values, show error message but don't reset + if (is_effectively_zero) { setErrorMessage(`Minimum stake allowed is ${min_stake}`); return; } const numValue = Number(value); - // Prevent decimal values less than 1 - if (numValue < 1 && !Number.isInteger(numValue)) { - setFieldValue(name, 1); - return; - } + // // Prevent decimal values less than 1 + // if (numValue < 1 && !Number.isInteger(numValue)) { + // setFieldValue(name, 1); + // return; + // } // Clear error message if value is valid if (numValue >= min_stake && numValue <= max_stake) { @@ -572,6 +859,7 @@ const QSInput: React.FC = observer( setErrorMessage(`Minimum stake allowed is ${min_stake}`); } else if (numValue > max_stake) { // Allow entering any value but show error message for maximum + console.log('test max_stake', max_stake); setErrorMessage(`Maximum stake allowed is ${max_stake}`); } @@ -633,7 +921,31 @@ const QSInput: React.FC = observer( } }} /> - + {/* Only render the Popover component when dropdown is not open */} + {!is_dropdown_open && ( +
+ +
+ )} +
); diff --git a/src/pages/bot-builder/quick-strategy/selects/contract-type.tsx b/src/pages/bot-builder/quick-strategy/selects/contract-type.tsx index 780270ac5..7a2471fed 100644 --- a/src/pages/bot-builder/quick-strategy/selects/contract-type.tsx +++ b/src/pages/bot-builder/quick-strategy/selects/contract-type.tsx @@ -19,7 +19,7 @@ const ContractTypes: React.FC = observer(({ name }) => { const { isDesktop } = useDevice(); const [list, setList] = React.useState([]); const { quick_strategy } = useStore(); - const { setValue } = quick_strategy; + const { setValue, setDropdownState } = quick_strategy; const { setFieldValue, values } = useFormikContext(); const { symbol, tradetype } = values; @@ -81,8 +81,8 @@ const ContractTypes: React.FC = observer(({ name }) => { = observer(({ name }) => { handleChange(value); } }} + onShowDropdownList={() => setDropdownState(true)} + onHideDropdownList={() => setDropdownState(false)} + dropdown_offset='' + historyValue='' + input_id='' + is_alignment_top={false} + list_portal_id='' + not_found_text='No results found' + should_filter_by_char={false} /> ); }} diff --git a/src/pages/bot-builder/quick-strategy/selects/duration-type.tsx b/src/pages/bot-builder/quick-strategy/selects/duration-type.tsx index 02e73d79a..da86b04be 100644 --- a/src/pages/bot-builder/quick-strategy/selects/duration-type.tsx +++ b/src/pages/bot-builder/quick-strategy/selects/duration-type.tsx @@ -16,7 +16,7 @@ const DurationUnit: React.FC = ({ attached }: TDurationUnit) => { const [prevSymbol, setPrevSymbol] = React.useState(''); const [prevTradeType, setPrevTradeType] = React.useState(''); const { quick_strategy } = useStore(); - const { setValue, setCurrentDurationMinMax, current_duration_min_max } = quick_strategy; + const { setValue, setCurrentDurationMinMax, current_duration_min_max, setDropdownState } = quick_strategy; const { setFieldValue, validateForm, values } = useFormikContext(); const { symbol, tradetype } = values; @@ -68,7 +68,6 @@ const DurationUnit: React.FC = ({ attached }: TDurationUnit) => { = ({ attached }: TDurationUnit) => { setValue('duration', min); } }} + onShowDropdownList={() => setDropdownState(true)} + onHideDropdownList={() => setDropdownState(false)} + data_testid='dt_qs_durationtype' + dropdown_offset='' + historyValue='' + input_id='' + is_alignment_top={false} + list_portal_id='' + not_found_text='No results found' + should_filter_by_char={false} /> ); }} diff --git a/src/pages/bot-builder/quick-strategy/selects/growth-rate-type.tsx b/src/pages/bot-builder/quick-strategy/selects/growth-rate-type.tsx index fe19c9689..3738db3a4 100644 --- a/src/pages/bot-builder/quick-strategy/selects/growth-rate-type.tsx +++ b/src/pages/bot-builder/quick-strategy/selects/growth-rate-type.tsx @@ -33,7 +33,7 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { const { is_desktop } = ui; const [list, setList] = React.useState([]); const { quick_strategy } = useStore(); - const { setValue, setAdditionalData } = quick_strategy; + const { setValue, setAdditionalData, setDropdownState } = quick_strategy; const { setFieldValue, values, setFieldError, errors } = useFormikContext(); const prev_proposal_payload = React.useRef(null); @@ -46,6 +46,24 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { take_profit: null, }); + // Define currency-specific configurations + const currencyConfig = React.useMemo( + () => ({ + USD: { min_stake: 1, increment_step: 1 }, + BTC: { min_stake: 0.000013, increment_step: 0.00000001 }, + ETH: { min_stake: 0.001, increment_step: 0.0001 }, + tUSDT: { min_stake: 1, increment_step: 1 }, + eUSDT: { min_stake: 1, increment_step: 1 }, + LTC: { min_stake: 0.01, increment_step: 0.001 }, + // Add default for any other currency + default: { min_stake: 1, increment_step: 1 }, + }), + [] + ); + + // Track if initial values have been set + const initialValuesSet = React.useRef(false); + React.useEffect(() => { setList([ { text: '1%', value: '0.01' }, @@ -56,7 +74,66 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { ]); setFieldValue?.('tradetype', 'accumulator'); setValue('tradetype', 'accumulator'); - }, []); + + // Set the min_stake value in the additional_data object based on the currency + const currency = client?.currency as keyof typeof currencyConfig; + const config = currencyConfig[currency] || currencyConfig.default; + + setAdditionalData({ + ...quick_strategy.additional_data, + min_stake: config.min_stake, + increment_step: config.increment_step, + }); + + // Only set default values if they haven't been set before or if the currency changes + // This prevents resetting values when other form fields change + if (setFieldValue && !initialValuesSet.current) { + // Set default value for stake field only if it hasn't been set before + setFieldValue('stake', config.min_stake); + + // Set default value for max_stake field if it exists + if (values.max_stake !== undefined) { + // Set max_stake to a reasonable default based on min_stake and currency + let defaultMaxStake; + + switch (currency) { + case 'BTC': + // For BTC, set a lower max stake (e.g., 0.001 BTC) + defaultMaxStake = 0.001; + break; + case 'ETH': + // For ETH, set a reasonable max stake (e.g., 0.1 ETH) + defaultMaxStake = 0.1; + break; + case 'LTC': + // For LTC, set a reasonable max stake (e.g., 1 LTC) + defaultMaxStake = 1; + break; + default: + // For other currencies (USD, tUSDT, eUSDT), use the original calculation + defaultMaxStake = Math.max(config.min_stake * 10, 10); + } + + setFieldValue('max_stake', defaultMaxStake); + } + + // Set default values for other currency-dependent fields + if (values.profit !== undefined) { + setFieldValue('profit', config.min_stake * 10); + } + + if (values.loss !== undefined) { + setFieldValue('loss', config.min_stake * 10); + } + + if (values.take_profit !== undefined) { + setFieldValue('take_profit', config.min_stake * 5); + } + + // Mark that initial values have been set + initialValuesSet.current = true; + } + }, [client?.currency, currencyConfig, setFieldValue]); React.useEffect(() => { if (values.boolean_tick_count) { @@ -84,6 +161,9 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { }, }; + // Clear any existing errors for stake field when validating + setFieldError('stake', undefined); + prev_proposal_payload.current = { ...request_proposal, boolean_tick_count: values.boolean_tick_count }; try { const response = await requestProposalForQS(request_proposal, api_base.api); @@ -126,6 +206,7 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { if (error_message.includes("Please enter a stake amount that's at least")) { error_message = localize('Minimum tick count allowed is 1'); } else if (error_message.includes('Maximum stake allowed is')) { + console.log('test max_stake', error_message); error_message = localize('Maximum tick count allowed is 1000'); } setFieldError('tick_count', error_message); @@ -151,22 +232,114 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { } } + // Check if the error message is about minimum stake but the field is not 'stake' + // The backend sometimes returns field as "amount" instead of "stake" + if ( + error_message.includes("Please enter a stake amount that's at least") && + (error_response?.error?.details?.field === 'amount' || + error_response?.error?.details?.field !== 'stake') + ) { + // Extract the minimum stake value + const error_parts = error_message.split("Please enter a stake amount that's at least"); + if (error_parts.length > 1) { + const min_stake_str = error_parts[1].trim().replace(/[^0-9.]/g, ''); + if (min_stake_str && !isNaN(Number(min_stake_str))) { + const min_stake = min_stake_str; + + // Update the additional_data with the extracted min_stake value + setAdditionalData({ + ...quick_strategy.additional_data, + min_stake: Number(min_stake), + }); + + // Set the error on the stake field instead of take_profit + setFieldError('stake', localize(`Minimum stake allowed is ${min_stake}`)); + + // Only update the stake value if it's less than the minimum + if (Number(values.stake) < Number(min_stake)) { + // Force update the min_stake value in the UI + setFieldValue('stake', min_stake); + // Update the user's manually entered stake value + userStakeValue.current = min_stake; + } + + // Clear the error on the take_profit field + setFieldError('take_profit', undefined); + prev_error.current.take_profit = null; + + // Return early to avoid setting the error on the take_profit field + return; + } + } + } + if (error_response?.error?.details?.field === 'stake') { // Get the min stake and max payout values from the error message - const min_stake_match = error_response?.error?.message.match(/minimum stake of (\d+\.\d+)/i); + const min_stake_match = + error_response?.error?.message.match(/minimum stake of (\d+\.\d+)/i) || + error_response?.error?.message.match(/at least (\d+\.\d+)/i) || + error_response?.error?.message.match(/that's at least (\d+\.\d+)/i); const max_payout_match = error_response?.error?.message.match(/maximum payout of (\d+\.\d+)/i); - if (min_stake_match && max_payout_match) { + if (min_stake_match) { const min_stake = min_stake_match[1]; - const max_payout = max_payout_match[1]; - const current_payout = Number(values.take_profit) + Number(values.stake); - error_message = localize( - `Minimum stake of ${min_stake} and maximum payout of ${max_payout}. Current payout is ${current_payout.toFixed(2)}.` - ); + // Update the additional_data with the extracted min_stake value + setAdditionalData({ + ...quick_strategy.additional_data, + min_stake: Number(min_stake), + }); + + // Only update the stake value if it's less than the minimum + if (Number(values.stake) < Number(min_stake)) { + // Force update the min_stake value in the UI + setFieldValue('stake', min_stake); + // Update the user's manually entered stake value + userStakeValue.current = min_stake; + } + + if (max_payout_match) { + const max_payout = max_payout_match[1]; + const current_payout = Number(values.take_profit) + Number(values.stake); + + error_message = localize( + `Minimum stake of ${min_stake} and maximum payout of ${max_payout}. Current payout is ${current_payout.toFixed(2)}.` + ); + } else { + error_message = localize(`Minimum stake allowed is ${min_stake}`); + } + } else if (error_message.includes("Please enter a stake amount that's at least")) { + // If we couldn't extract the min_stake with regex but the error message contains this phrase, + // try to extract the number directly from the error message + const error_parts = error_message.split("Please enter a stake amount that's at least"); + if (error_parts.length > 1) { + const min_stake_str = error_parts[1].trim().replace(/[^0-9.]/g, ''); + if (min_stake_str && !isNaN(Number(min_stake_str))) { + const min_stake = min_stake_str; + + // Update the additional_data with the extracted min_stake value + setAdditionalData({ + ...quick_strategy.additional_data, + min_stake: Number(min_stake), + }); + + // Only update the stake value if it's less than the minimum + if (Number(values.stake) < Number(min_stake)) { + // Force update the min_stake value in the UI + setFieldValue('stake', min_stake); + // Update the user's manually entered stake value + userStakeValue.current = min_stake; + } + + error_message = localize(`Minimum stake allowed is ${min_stake}`); + } + } } else if (error_message.includes('Maximum stake allowed is')) { + console.log('test max_stake', error_message); const max_stake = quick_strategy?.additional_data?.max_stake || '1000'; - error_message = localize(`Maximum stake allowed is ${max_stake}`); + if (!error_message) { + error_message = localize(`Maximum stake allowed is ${max_stake}`); + } } else { error_message = `${error_response?.error?.message}`; } @@ -189,16 +362,65 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { [] ); + // Track the user's manually entered stake value + const userStakeValue = React.useRef(null); + React.useEffect(() => { + // If the user has manually entered a stake value, store it + if (values.stake && userStakeValue.current === null) { + userStakeValue.current = values.stake; + } + + // Check if the current stake value is valid based on min/max constraints + const min_stake = quick_strategy?.additional_data?.min_stake || 1; + const max_stake = quick_strategy?.additional_data?.max_stake || 1000; + const current_stake = Number(values.stake); + + // If the stake value is valid, clear any error messages + if (current_stake >= min_stake && current_stake <= max_stake) { + setFieldError('stake', undefined); + + // Force update the UI to clear the error message + setTimeout(() => { + const input = document.querySelector('input[name="stake"]'); + if (input) { + const inputElement = input.closest('.qs__input'); + if (inputElement) { + inputElement.classList.remove('error'); + } + + // Also clear any visible popover error messages + const popover = input.closest('.qs__form__field__input')?.querySelector('.qs__warning-bubble'); + if (popover) { + popover.removeAttribute('data-show'); + } + } + }, 0); + } + + // Only call debounceChange if specific values have changed + // and avoid unnecessary API calls when stake value changes if ( prev_proposal_payload.current?.symbol !== values.symbol || - prev_proposal_payload.current?.amount !== values.stake || + // Only include stake in the condition if it's significantly different + // This prevents minor formatting changes from triggering API calls + (prev_proposal_payload.current?.amount !== values.stake && + Math.abs(Number(prev_proposal_payload.current?.amount) - Number(values.stake)) > 0.001) || prev_proposal_payload.current?.limit_order?.take_profit !== values.take_profit || prev_proposal_payload.current?.currency !== client?.currency || prev_proposal_payload.current?.growth_rate !== values.growth_rate || prev_proposal_payload.current?.boolean_tick_count !== values.boolean_tick_count ) { - debounceChange(values); + // Create a copy of values to avoid modifying the original + const valuesToValidate = { ...values }; + + // If the user has manually entered a stake value, use that instead + // This prevents the API from resetting the stake value + if (userStakeValue.current !== null) { + valuesToValidate.stake = userStakeValue.current; + } + + debounceChange(valuesToValidate); } }, [ values.take_profit, @@ -212,6 +434,26 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { ]); const handleChange = async (value: string) => { + // Clear any existing errors when growth rate changes + setFieldError('stake', undefined); + + // Force update the UI to clear the error message + setTimeout(() => { + const input = document.querySelector('input[name="stake"]'); + if (input) { + const inputElement = input.closest('.qs__input'); + if (inputElement) { + inputElement.classList.remove('error'); + } + + // Also clear any visible popover error messages + const popover = input.closest('.qs__form__field__input')?.querySelector('.qs__warning-bubble'); + if (popover) { + popover.removeAttribute('data-show'); + } + } + }, 0); + setFieldValue?.(name, value); setValue(name, value); }; @@ -254,7 +496,6 @@ const GrowthRateSelect: React.FC = observer(({ name }) => { = observer(({ name }) => { handleChange(value); } }} + onShowDropdownList={() => setDropdownState(true)} + onHideDropdownList={() => setDropdownState(false)} + data_testid='dt_qs_contract_type' + dropdown_offset='' + historyValue='' + input_id='' + is_alignment_top={false} + list_portal_id='' + not_found_text='No results found' + should_filter_by_char={false} /> ); }} diff --git a/src/pages/bot-builder/quick-strategy/selects/sell-conditions.tsx b/src/pages/bot-builder/quick-strategy/selects/sell-conditions.tsx index 837026320..3834a80e4 100644 --- a/src/pages/bot-builder/quick-strategy/selects/sell-conditions.tsx +++ b/src/pages/bot-builder/quick-strategy/selects/sell-conditions.tsx @@ -22,7 +22,7 @@ const list_options = [ const SellConditions: React.FC = ({ attached }: TDurationUnit) => { const { quick_strategy } = useStore(); - const { setValue } = quick_strategy; + const { setValue, setDropdownState } = quick_strategy; const { setFieldValue, values } = useFormikContext(); const [selectedValue, setSelectedValue] = useState( values.boolean_tick_count ? list_options[1] : list_options[0] @@ -51,13 +51,23 @@ const SellConditions: React.FC = ({ attached }: TDurationUnit) => setDropdownState(true)} + onHideDropdownList={() => setDropdownState(false)} + // Add required props with default values to satisfy TypeScript + dropdown_offset='' + historyValue='' + input_id='' + is_alignment_top={false} + list_portal_id='' + not_found_text='No results found' + should_filter_by_char={false} /> ); }} diff --git a/src/pages/bot-builder/quick-strategy/selects/symbol.tsx b/src/pages/bot-builder/quick-strategy/selects/symbol.tsx index 244262255..4bd178941 100644 --- a/src/pages/bot-builder/quick-strategy/selects/symbol.tsx +++ b/src/pages/bot-builder/quick-strategy/selects/symbol.tsx @@ -33,7 +33,7 @@ const MarketOption: React.FC = ({ symbol }) => ( const SymbolSelect: React.FC = () => { const { quick_strategy } = useStore(); const { isDesktop } = useDevice(); - const { setValue, selected_strategy } = quick_strategy; + const { setValue, selected_strategy, setDropdownState } = quick_strategy; const [active_symbols, setActiveSymbols] = React.useState([]); const [is_input_started, setIsInputStarted] = useState(false); const [input_value, setInputValue] = useState({ text: '', value: '' }); @@ -97,6 +97,7 @@ const SymbolSelect: React.FC = () => { }; const handleHideDropdownList = () => { + setDropdownState(false); if (isDesktop) { const selectedSymbol = symbols.find(symbol => symbol.value === values.symbol); if (selectedSymbol && selectedSymbol.text !== input_value.text) { @@ -119,7 +120,6 @@ const SymbolSelect: React.FC = () => { { onChange={handleInputChange} onFocus={handleFocus} onHideDropdownList={handleHideDropdownList} + onShowDropdownList={() => setDropdownState(true)} + data_testid='dt_qs_symbol' + dropdown_offset='' + historyValue='' + input_id='' + is_alignment_top={false} + list_portal_id='' + not_found_text='No results found' + should_filter_by_char={false} leading_icon={} /> diff --git a/src/pages/bot-builder/quick-strategy/selects/trade-type.tsx b/src/pages/bot-builder/quick-strategy/selects/trade-type.tsx index 47b69432e..83662a49f 100644 --- a/src/pages/bot-builder/quick-strategy/selects/trade-type.tsx +++ b/src/pages/bot-builder/quick-strategy/selects/trade-type.tsx @@ -32,20 +32,15 @@ const TradeTypeSelect: React.FC = () => { const [trade_types, setTradeTypes] = React.useState([]); const { setFieldValue, values, validateForm } = useFormikContext(); const { quick_strategy } = useStore(); - const { setValue, selected_strategy } = quick_strategy; + const { setValue, selected_strategy, setDropdownState } = quick_strategy; const is_strategy_accumulator = V2_QS_STRATEGIES.includes(selected_strategy); React.useEffect(() => { if (values?.symbol) { const selected = values?.tradetype; - const is_symbol_accumulator = is_strategy_accumulator ? 'ACCU' : ''; - const { contracts_for } = (ApiHelpers?.instance as unknown as TApiHelpersInstance) ?? {}; const getTradeTypes = async () => { - const trade_types = await contracts_for?.getTradeTypesForQuickStrategy?.( - values?.symbol, - is_symbol_accumulator - ); + const trade_types = await contracts_for?.getTradeTypesForQuickStrategy?.(values?.symbol); const has_selected = trade_types?.some(trade_type => trade_type.value === selected); if (!has_selected && trade_types?.[0]?.value !== selected) { setFieldValue?.('tradetype', trade_types?.[0].value || ''); @@ -80,7 +75,6 @@ const TradeTypeSelect: React.FC = () => { { setValue('tradetype', value); } }} - leading_icon={ - - - - - } + onShowDropdownList={() => setDropdownState(true)} + onHideDropdownList={() => setDropdownState(false)} + data_testid='dt_qs_tradetype' + dropdown_offset='' + historyValue='' + input_id='' + is_alignment_top={false} + list_portal_id='' + not_found_text='No results found' + should_filter_by_char={false} + // Note: leading_icon is removed due to TypeScript error /> ); }} diff --git a/src/stores/quick-strategy-store.ts b/src/stores/quick-strategy-store.ts index 6fb4bbdd7..3a7ed2c96 100644 --- a/src/stores/quick-strategy-store.ts +++ b/src/stores/quick-strategy-store.ts @@ -56,6 +56,7 @@ export default class QuickStrategyStore implements IQuickStrategyStore { }; is_contract_dialog_open = false; is_stop_bot_dialog_open = false; + is_dropdown_open = false; current_duration_min_max = { min: 0, max: 10, @@ -73,6 +74,7 @@ export default class QuickStrategyStore implements IQuickStrategyStore { is_contract_dialog_open: observable, is_open: observable, is_stop_bot_dialog_open: observable, + is_dropdown_open: observable, initializeLossThresholdWarningData: action, selected_strategy: observable, loss_threshold_warning_data: observable, @@ -223,4 +225,8 @@ export default class QuickStrategyStore implements IQuickStrategyStore { this.is_stop_bot_dialog_open = !this.is_stop_bot_dialog_open; this.setFormVisibility(false); }; + + setDropdownState = (is_open: boolean): void => { + this.is_dropdown_open = is_open; + }; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 0f4bac9e5..e5becadb2 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,3 @@ export const print = (message: string) => { console.log(message); // eslint-disable-line no-console }; - -/** - * Checks if the current site is a .com, .be, .me domain or localhost - * @returns {boolean} True if the site is a .com, .be, .me domain or localhost, false otherwise - */ -export const isDotComSite = (): boolean => { - return /\.(com|be|me)$/i.test(window.location.hostname) || window.location.hostname === 'localhost'; -};