) =>
+ e.key === 'Enter' && handleMouseDown(null, DRAGGABLE_CONSTANTS.MOVE)
+ }
+ onMouseDown={e => handleMouseDown(e, DRAGGABLE_CONSTANTS.BOTTOM_RIGHT)}
+ tabIndex={0}
+ />
+
+ );
+};
+
+export default Draggable;
diff --git a/src/components/draggable/index.ts b/src/components/draggable/index.ts
new file mode 100644
index 00000000..35d43c95
--- /dev/null
+++ b/src/components/draggable/index.ts
@@ -0,0 +1,5 @@
+import Draggable from './draggable';
+
+import './draggable.scss';
+
+export default Draggable;
diff --git a/src/components/flyout/flyout-block-group.tsx b/src/components/flyout/flyout-block-group.tsx
new file mode 100644
index 00000000..4396e703
--- /dev/null
+++ b/src/components/flyout/flyout-block-group.tsx
@@ -0,0 +1,76 @@
+import classNames from 'classnames';
+
+import { Button, Text } from '@deriv-com/ui';
+
+import { Localize } from '@/utils/tmp/dummy';
+
+import FlyoutBlock from './flyout-block';
+
+type TFlyoutBlockGroup = {
+ onInfoClick: () => void;
+ block_node: Element;
+ is_active: boolean;
+ should_hide_display_name: boolean;
+};
+
+const FlyoutBlockGroup = ({ onInfoClick, block_node, is_active, should_hide_display_name }: TFlyoutBlockGroup) => {
+ const block_type = (block_node.getAttribute('type') || '') as string;
+ const block_meta = window.Blockly.Blocks[block_type].meta();
+ const is_variables_get = block_type === 'variables_get';
+ const is_variables_set = block_type === 'variables_set';
+ const { display_name, description } = block_meta;
+
+ const AddButton = () => (
+
+ >
+ );
+};
+
+export default FlyoutBlockGroup;
diff --git a/src/components/flyout/flyout-block.tsx b/src/components/flyout/flyout-block.tsx
new file mode 100644
index 00000000..b88b8238
--- /dev/null
+++ b/src/components/flyout/flyout-block.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { useStore } from '@/hooks/useStore';
+
+type FlyoutBlockProps = {
+ block_node: Element;
+ should_hide_display_name?: boolean;
+};
+
+const FlyoutBlock = observer(({ block_node, should_hide_display_name }: FlyoutBlockProps) => {
+ const { flyout } = useStore();
+ const { initBlockWorkspace } = flyout;
+
+ let el_block_workspace = React.useRef();
+
+ React.useEffect(() => {
+ initBlockWorkspace(el_block_workspace, block_node);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
(el_block_workspace = el)}
+ className={classNames({
+ 'flyout__block-workspace--center': should_hide_display_name,
+ 'flyout__block-workspace--top': !should_hide_display_name,
+ })}
+ data-testid='flyout-block-workspace'
+ />
+ );
+});
+
+export default FlyoutBlock;
diff --git a/src/components/flyout/flyout.scss b/src/components/flyout/flyout.scss
new file mode 100644
index 00000000..73031f74
--- /dev/null
+++ b/src/components/flyout/flyout.scss
@@ -0,0 +1,229 @@
+@keyframes fade-in {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.flyout {
+ $flyout: &;
+ $default-margin: 15px;
+ $button-padding: 5px 20px;
+
+ position: absolute;
+ inset-inline-start: 250px;
+ top: 0;
+ background-color: var(--general-main-2);
+ height: calc(100vh - 232px);
+ max-height: calc(100vh - 232px);
+ z-index: 11;
+ border-radius: $BORDER_RADIUS;
+ font-size: 2em;
+ margin-inline-start: $default-margin;
+ margin-top: 20px;
+ box-shadow: 0 2px 8px 0 var(--shadow-box);
+ min-width: 400px;
+ visibility: hidden;
+
+ &__item:hover {
+ .flyout__button-add--hide {
+ display: flex !important;
+ animation: fade-in 0.3s;
+ }
+ }
+ &__content {
+ overflow: auto;
+ height: calc(100% - 64px);
+
+ .dc-themed-scrollbars {
+ padding: 5px 25px;
+ }
+ &-disclaimer {
+ display: flex;
+ justify-content: space-around;
+ background: $color-yellow;
+ font-size: var(--text-size-xs);
+ margin-top: 1.6em;
+ line-height: 1.3em;
+ padding: 0.8rem;
+ border-radius: 4px;
+
+ &-text {
+ color: $color-black-1;
+ width: 324px;
+ }
+ &-icon {
+ padding-top: 0.8rem;
+ }
+ }
+ }
+ &__block-workspace {
+ &--top {
+ margin-bottom: $default-margin;
+ }
+
+ &--center {
+ margin-top: 0.6em;
+ .injectionDiv {
+ height: 100%;
+ }
+ }
+ &__header {
+ display: flex;
+ }
+ }
+ &__button {
+ &-new {
+ width: 20%;
+ height: 4rem !important;
+ font-size: var(--text-size-xs);
+ font-weight: bold;
+ border-top-left-radius: 0rem !important;
+ border-bottom-left-radius: 0rem !important;
+ }
+ &-add {
+ color: var(--general-main-1);
+
+ &--hide {
+ display: none !important;
+ }
+ }
+ &-back {
+ padding: 0 15px;
+ align-self: center;
+ background-color: transparent;
+ color: $COLOR_BLACK;
+
+ svg {
+ vertical-align: middle;
+ }
+ &:focus {
+ outline: none;
+ }
+ }
+ &-next,
+ &-previous {
+ margin-inline-start: 1em;
+ color: $COLOR_LIGHT_BLACK_1;
+ background-color: var(--general-section-1);
+ display: flex;
+ }
+ }
+ &__item {
+ line-height: 1.3em;
+ font-size: var(--text-size-xs);
+
+ &:not(:last-of-type) {
+ margin-bottom: 30px;
+ }
+ &-header {
+ display: flex;
+ margin-top: $default-margin;
+ margin-bottom: 10px;
+ }
+ &-buttons {
+ margin-inline-start: auto;
+ align-self: center;
+ }
+ &-info {
+ cursor: pointer;
+ font-weight: bold;
+ display: block;
+ color: $COLOR_RED;
+ }
+ &-description {
+ font-size: var(--text-size-xs);
+ margin-bottom: 1em;
+ line-height: 1.3em;
+ color: var(--text-general);
+ }
+ }
+ &__image {
+ width: 100%;
+ height: auto;
+ border-radius: 0.5em;
+ }
+ &__video {
+ width: 100%;
+ height: 20vh;
+ border-radius: 0.5em;
+ }
+ &__help {
+ padding: 0;
+ height: 100%;
+ visibility: visible;
+
+ &-header {
+ padding: 15px;
+ display: flex;
+ background-color: var(--general-section-1);
+ }
+ &-content {
+ padding: 1.5em;
+ font-size: 0.8em;
+ overflow-y: auto;
+ height: calc(100vh - 295px);
+
+ #{$flyout}__item {
+ margin-bottom: 0.8em;
+ }
+ }
+ &-title {
+ align-self: center;
+ }
+ &-footer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 0.5em 0.8em;
+ border-top: solid 0.1em var(--general-section-1);
+ }
+ }
+ &__search {
+ padding: 0;
+ visibility: visible;
+
+ &-header {
+ padding: 20px;
+ background-color: var(--general-disabled);
+ display: flex;
+ justify-content: space-between;
+
+ &-text {
+ align-self: center;
+ }
+ }
+ &-empty {
+ padding: 25px 0;
+ }
+ #{$flyout}__help-content {
+ height: calc(100% - 60px);
+ }
+ }
+ &__normal {
+ visibility: visible;
+
+ &-content {
+ height: 100%;
+ }
+ }
+ &__input {
+ width: 80% !important;
+ height: 4rem;
+ border-top-right-radius: 0rem !important;
+ border-bottom-right-radius: 0rem !important;
+ border: solid 1px $color-grey-5 !important;
+ display: inline-block !important;
+ margin-top: 3.3rem;
+ }
+ &__hr {
+ height: 2px;
+ width: 100%;
+ border-top: 1px solid var(--general-section-1);
+ position: absolute;
+ left: 0;
+ right: 0;
+ margin: 0;
+ }
+}
diff --git a/src/components/flyout/flyout.tsx b/src/components/flyout/flyout.tsx
new file mode 100644
index 00000000..d5afa4d5
--- /dev/null
+++ b/src/components/flyout/flyout.tsx
@@ -0,0 +1,248 @@
+import React from 'react';
+import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { Input, Text } from '@deriv-com/ui';
+
+import { useStore } from '@/hooks/useStore';
+import { help_content_config } from '@/utils/help-content/help-content.config';
+import { Icon, localize } from '@/utils/tmp/dummy';
+
+import { getPlatformSettings } from '../shared';
+import ThemedScrollbars from '../shared_ui/themed-scrollbars';
+
+import FlyoutBlockGroup from './flyout-block-group';
+import HelpBase from './help-contents';
+
+type TSearchResult = {
+ search_term: string;
+ total_result: number;
+};
+
+const SearchResult = ({ search_term, total_result }: TSearchResult) => (
+
+
+ {localize('Results for "{{ search_term }}"', {
+ search_term,
+ interpolation: { escapeValue: false },
+ })}
+
+
+ {`${total_result} ${total_result > 1 ? localize('results') : localize('result')}`}
+
+
+);
+
+type TFlyoutContent = {
+ flyout_content: Element[];
+ active_helper: string;
+ setHelpContent: (node: Element) => void;
+ initFlyoutHelp: (node: Element, block_type: string) => void;
+ is_empty: boolean;
+ is_search_flyout: boolean;
+ selected_category: Element;
+ first_get_variable_block_index: number;
+};
+
+const FlyoutContent = (props: TFlyoutContent) => {
+ const flyout_ref = React.useRef();
+ const {
+ flyout_content,
+ active_helper,
+ setHelpContent,
+ initFlyoutHelp,
+ is_empty,
+ is_search_flyout,
+ selected_category,
+ first_get_variable_block_index,
+ } = props;
+
+ return (
+
+
+ {selected_category?.getAttribute('id') === 'indicators' && (
+
+
+
+
+
+ {localize(
+ 'Indicators on the chart tab are for indicative purposes only and may vary slightly from the ones on the {{platform_name_dbot}} workspace.',
+ { platform_name_dbot: getPlatformSettings('dbot').name }
+ )}
+
+
+ )}
+ {is_empty ? (
+
+
+ {localize('No results found')}
+
+
+ ) : (
+ flyout_content.map((node, index) => {
+ const tag_name = node.tagName.toUpperCase();
+ switch (tag_name) {
+ case window.Blockly.Xml.NODE_BLOCK: {
+ const block_type = (node.getAttribute('type') || '') as string;
+
+ return (
+ setHelpContent(node)
+ : () => initFlyoutHelp(node, block_type))
+ }
+ is_active={active_helper === block_type}
+ />
+ );
+ }
+ case window.Blockly.Xml.NODE_LABEL: {
+ return (
+
+ {node.getAttribute('text')}
+
+ );
+ }
+ case window.Blockly.Xml.NODE_INPUT: {
+ return (
+
+ );
+ }
+ case window.Blockly.Xml.NODE_BUTTON: {
+ const callback_key = node.getAttribute('callbackKey');
+ const callback_id = node.getAttribute('id') as string;
+
+ return (
+ {
+ const workspace = window.Blockly.derivWorkspace;
+ const button_cb = workspace.getButtonCallback(callback_key);
+ const callback = button_cb;
+
+ // Workaround for not having a flyout workspace.
+ // eslint-disable-next-line no-underscore-dangle
+ button.targetWorkspace_ = workspace;
+ button.getTargetWorkspace = () => {
+ // eslint-disable-next-line no-underscore-dangle
+ return button.targetWorkspace_;
+ };
+
+ callback?.(button);
+ }}
+ >
+ {node.getAttribute('text')}
+
+ );
+ }
+ default:
+ return null;
+ }
+ })
+ )}
+
+
+ );
+};
+
+const Flyout = observer(() => {
+ const { flyout, flyout_help } = useStore();
+ const { gtm } = useStore();
+ const { active_helper, initFlyoutHelp, setHelpContent } = flyout_help;
+ const {
+ flyout_content,
+ flyout_width,
+ is_help_content,
+ is_search_flyout,
+ is_visible,
+ onMount,
+ onUnmount,
+ search_term,
+ selected_category,
+ first_get_variable_block_index,
+ } = flyout;
+
+ const { pushDataLayer } = gtm;
+
+ React.useEffect(() => {
+ onMount();
+ return () => onUnmount();
+ }, [onMount, onUnmount]);
+
+ if (is_visible && is_search_flyout) {
+ pushDataLayer({ event: 'dbot_search_results', value: true });
+ }
+
+ const total_result = Object.keys(flyout_content).length;
+ const is_empty = total_result === 0;
+
+ return (
+ is_visible && (
+
+ {is_search_flyout && !is_help_content && (
+
+ )}
+ {is_help_content ? (
+
+ ) : (
+
+ )}
+
+ )
+ );
+});
+
+export default Flyout;
diff --git a/src/components/flyout/help-contents/flyout-help-base.tsx b/src/components/flyout/help-contents/flyout-help-base.tsx
new file mode 100644
index 00000000..077774fd
--- /dev/null
+++ b/src/components/flyout/help-contents/flyout-help-base.tsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+
+import { Button, Text } from '@deriv-com/ui';
+
+import { useStore } from '@/hooks/useStore';
+import { help_content_config, help_content_types } from '@/utils/help-content/help-content.config';
+import { Icon, localize } from '@/utils/tmp/dummy';
+
+import FlyoutBlock from '../flyout-block';
+
+import FlyoutImage from './flyout-img';
+import FlyoutText from './flyout-text';
+import FlyoutVideo from './flyout-video';
+
+const HelpBase = observer(() => {
+ const { flyout, flyout_help } = useStore();
+ const {
+ block_node,
+ block_type,
+ examples,
+ help_string,
+ onBackClick,
+ onSequenceClick,
+ should_next_disable,
+ should_previous_disable,
+ title,
+ } = flyout_help;
+ const { is_search_flyout } = flyout;
+
+ const block_help_component = help_string && help_content_config(__webpack_public_path__)[block_type];
+ let text_count = 0;
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ Blockly.derivWorkspace.addBlockNode(block_node)}
+ primary
+ text={localize('Add')}
+ type='button'
+ />
+
+
+
+ {block_help_component &&
+ block_help_component.map((component, index) => {
+ const { type, width, url, example_id } = component;
+ const { text } = help_string;
+ const example_node = examples.find(example => example.id === example_id);
+ switch (type) {
+ case help_content_types.TEXT:
+ if (text_count < text.length) {
+ return ;
+ }
+ return null;
+ case help_content_types.VIDEO:
+ return ;
+ case help_content_types.IMAGE:
+ return ;
+ case help_content_types.BLOCK: {
+ return ;
+ }
+ case help_content_types.EXAMPLE:
+ if (example_node) {
+ return (
+
+ );
+ }
+ return null;
+ default:
+ return null;
+ }
+ })}
+
+ {!is_search_flyout && !(should_previous_disable && should_next_disable) && (
+
+ onSequenceClick(false)}
+ text={localize('Previous')}
+ type='button'
+ is_disabled={should_previous_disable}
+ renderText={text =>
+ should_previous_disable && (
+
+ {text}
+
+ )
+ }
+ />
+ onSequenceClick(true)}
+ text={localize('Next')}
+ type='button'
+ is_disabled={should_next_disable}
+ renderText={text =>
+ should_next_disable && (
+
+ {text}
+
+ )
+ }
+ />
+
+ )}
+
+ );
+});
+
+export default HelpBase;
diff --git a/src/components/flyout/help-contents/flyout-img.tsx b/src/components/flyout/help-contents/flyout-img.tsx
new file mode 100644
index 00000000..533453b6
--- /dev/null
+++ b/src/components/flyout/help-contents/flyout-img.tsx
@@ -0,0 +1,17 @@
+type TFlyoutImageProps = {
+ width: string;
+ url: string;
+};
+
+const FlyoutImage = (props: TFlyoutImageProps) => {
+ const { width, url } = props;
+ const style = { width };
+
+ return (
+
+
+
+ );
+};
+
+export default FlyoutImage;
diff --git a/src/components/flyout/help-contents/flyout-text.tsx b/src/components/flyout/help-contents/flyout-text.tsx
new file mode 100644
index 00000000..0fec12ce
--- /dev/null
+++ b/src/components/flyout/help-contents/flyout-text.tsx
@@ -0,0 +1,13 @@
+import { Text } from '@deriv-com/ui';
+
+const FlyoutText = (props: { text: string }) => {
+ const { text } = props;
+
+ return (
+
+ {text}
+
+ );
+};
+
+export default FlyoutText;
diff --git a/src/components/flyout/help-contents/flyout-video.tsx b/src/components/flyout/help-contents/flyout-video.tsx
new file mode 100644
index 00000000..203ea81b
--- /dev/null
+++ b/src/components/flyout/help-contents/flyout-video.tsx
@@ -0,0 +1,15 @@
+const FlyoutVideo = (props: { url: string }) => (
+
+
+
+);
+
+export default FlyoutVideo;
diff --git a/src/components/flyout/help-contents/index.ts b/src/components/flyout/help-contents/index.ts
new file mode 100644
index 00000000..e15fe2de
--- /dev/null
+++ b/src/components/flyout/help-contents/index.ts
@@ -0,0 +1,3 @@
+import HelpBase from './flyout-help-base';
+
+export default HelpBase;
diff --git a/src/components/flyout/index.ts b/src/components/flyout/index.ts
new file mode 100644
index 00000000..b048a67b
--- /dev/null
+++ b/src/components/flyout/index.ts
@@ -0,0 +1,5 @@
+import Flyout from './flyout';
+
+import './flyout.scss';
+
+export default Flyout;
diff --git a/src/components/journal/index.ts b/src/components/journal/index.ts
new file mode 100644
index 00000000..90bc7b52
--- /dev/null
+++ b/src/components/journal/index.ts
@@ -0,0 +1,5 @@
+import Journal from './journal';
+
+import './journal.scss';
+
+export default Journal;
diff --git a/src/components/journal/journal-components/date-item.tsx b/src/components/journal/journal-components/date-item.tsx
new file mode 100644
index 00000000..6a1e7c2b
--- /dev/null
+++ b/src/components/journal/journal-components/date-item.tsx
@@ -0,0 +1,9 @@
+import { TDateItemProps } from '../journal.types';
+
+const DateItem = ({ date, time }: TDateItemProps) => (
+ <>
+
{date} |
{time}
+ >
+);
+
+export default DateItem;
diff --git a/src/components/journal/journal-components/filter-dialog.tsx b/src/components/journal/journal-components/filter-dialog.tsx
new file mode 100644
index 00000000..281071fe
--- /dev/null
+++ b/src/components/journal/journal-components/filter-dialog.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+import { useOnClickOutside } from '@/hooks/useOnClickOutside';
+
+import { TFilterDialogProps } from '../journal.types';
+
+import Filters from './filters';
+
+const FilterDialog = ({
+ toggle_ref,
+ checked_filters,
+ filters,
+ filterMessage,
+ is_filter_dialog_visible,
+ toggleFilterDialog,
+}: TFilterDialogProps) => {
+ const wrapper_ref = React.useRef
(null);
+ const validateClickOutside = (event: React.ChangeEvent) =>
+ is_filter_dialog_visible && !toggle_ref.current?.contains(event.target);
+
+ useOnClickOutside(wrapper_ref, toggleFilterDialog, validateClickOutside);
+
+ return (
+
+ );
+};
+
+export default FilterDialog;
diff --git a/src/components/journal/journal-components/filters.tsx b/src/components/journal/journal-components/filters.tsx
new file mode 100644
index 00000000..12118d15
--- /dev/null
+++ b/src/components/journal/journal-components/filters.tsx
@@ -0,0 +1,30 @@
+import { Checkbox } from '@deriv-com/ui';
+
+import { TFiltersProps } from '../journal.types';
+
+const Filters = ({
+ wrapper_ref,
+ checked_filters,
+ filters,
+ filterMessage,
+ className,
+ classNameLabel,
+}: TFiltersProps) => (
+
+ {filters.map(item => {
+ const hasFilter = Array.isArray(checked_filters) && checked_filters.includes(item.id);
+ return (
+ filterMessage(!hasFilter, item.id)}
+ />
+ );
+ })}
+
+);
+
+export default Filters;
diff --git a/src/components/journal/journal-components/format-message.tsx b/src/components/journal/journal-components/format-message.tsx
new file mode 100644
index 00000000..e4ca9608
--- /dev/null
+++ b/src/components/journal/journal-components/format-message.tsx
@@ -0,0 +1,100 @@
+import classnames from 'classnames';
+
+import { Text } from '@deriv-com/ui';
+
+import { formatMoney, getCurrencyDisplayCode } from '@/components/shared';
+import { LogTypes } from '@/external/bot-skeleton';
+import { Localize, localize } from '@/utils/tmp/dummy';
+
+import { TFormatMessageProps } from '../journal.types';
+
+const FormatMessage = ({ logType, className, extra }: TFormatMessageProps) => {
+ const getLogMessage = () => {
+ switch (logType) {
+ case LogTypes.LOAD_BLOCK: {
+ return localize('Blocks are loaded successfully');
+ }
+ case LogTypes.NOT_OFFERED: {
+ return localize('Resale of this contract is not offered.');
+ }
+ case LogTypes.PURCHASE: {
+ const { longcode, transaction_id } = extra;
+ return (
+ ]}
+ options={{ interpolation: { escapeValue: false } }}
+ />
+ );
+ }
+ case LogTypes.SELL: {
+ const { sold_for } = extra;
+ return (
+ ]}
+ />
+ );
+ }
+ case LogTypes.PROFIT: {
+ const { currency, profit } = extra;
+ return (
+ ]}
+ />
+ );
+ }
+ case LogTypes.LOST: {
+ const { currency, profit } = extra;
+ return (
+ ]}
+ />
+ );
+ }
+ case LogTypes.WELCOME_BACK: {
+ const { current_currency } = extra;
+ if (current_currency)
+ return (
+
+ );
+ return ;
+ }
+
+ case LogTypes.WELCOME: {
+ const { current_currency } = extra;
+ if (current_currency)
+ return (
+
+ );
+ break;
+ }
+ default:
+ return null;
+ }
+ };
+
+ return {getLogMessage()}
;
+};
+
+export default FormatMessage;
diff --git a/src/components/journal/journal-components/index.ts b/src/components/journal/journal-components/index.ts
new file mode 100644
index 00000000..ad72c129
--- /dev/null
+++ b/src/components/journal/journal-components/index.ts
@@ -0,0 +1,7 @@
+export { default as DateItem } from './date-item';
+export { default as FilterDialog } from './filter-dialog';
+export { default as Filters } from './filters';
+export { default as FormatMessage } from './format-message';
+export { default as JournalItem } from './journal-item';
+export { default as JournalLoader } from './journal-loader';
+export { default as JournalTools } from './journal-tools';
diff --git a/src/components/journal/journal-components/journal-item.tsx b/src/components/journal/journal-components/journal-item.tsx
new file mode 100644
index 00000000..86a9c70d
--- /dev/null
+++ b/src/components/journal/journal-components/journal-item.tsx
@@ -0,0 +1,55 @@
+import { CSSTransition } from 'react-transition-group';
+import classnames from 'classnames';
+
+import { MessageTypes } from '@/external/bot-skeleton';
+import { isDbotRTL } from '@/external/bot-skeleton/utils/workspace';
+import { useNewRowTransition } from '@/hooks/useNewRowTransition';
+
+import { TJournalItemExtra, TJournalItemProps } from '../journal.types';
+
+import DateItem from './date-item';
+import FormatMessage from './format-message';
+
+const getJournalItemContent = (
+ message: string | ((value: () => void) => string),
+ type: string,
+ className: string,
+ extra: TJournalItemExtra,
+ measure: () => void
+) => {
+ switch (type) {
+ case MessageTypes.SUCCESS: {
+ return ;
+ }
+ case MessageTypes.NOTIFY: {
+ if (typeof message === 'function') {
+ return {message(measure)}
;
+ }
+ return {message}
;
+ }
+ case MessageTypes.ERROR: {
+ return {message as string}
;
+ }
+ default:
+ return null;
+ }
+};
+
+const JournalItem = ({ row, is_new_row, measure }: TJournalItemProps) => {
+ const { in_prop } = useNewRowTransition(is_new_row);
+ const { date, time, message, message_type, className, extra } = row;
+ const date_el = DateItem({ date, time });
+
+ return (
+
+
+
+ {getJournalItemContent(message, message_type, className, extra as TJournalItemExtra, measure)}
+
+
{date_el}
+
+
+ );
+};
+
+export default JournalItem;
diff --git a/src/components/journal/journal-components/journal-loader.tsx b/src/components/journal/journal-components/journal-loader.tsx
new file mode 100644
index 00000000..0d3e6a1b
--- /dev/null
+++ b/src/components/journal/journal-components/journal-loader.tsx
@@ -0,0 +1,18 @@
+import ContentLoader from 'react-content-loader';
+import classnames from 'classnames';
+
+const JournalLoader = ({ is_mobile }: { is_mobile: boolean }) => (
+
+
+
+
+);
+
+export default JournalLoader;
diff --git a/src/components/journal/journal-components/journal-tools.tsx b/src/components/journal/journal-components/journal-tools.tsx
new file mode 100644
index 00000000..76b81f6e
--- /dev/null
+++ b/src/components/journal/journal-components/journal-tools.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { CSSTransition } from 'react-transition-group';
+
+import { Text } from '@deriv-com/ui';
+
+import Download from '@/components/download';
+import { Icon, Localize } from '@/utils/tmp/dummy';
+
+import { TJournalToolsProps } from '../journal.types';
+
+import FilterDialog from './filter-dialog';
+
+const JournalTools = ({
+ checked_filters,
+ filters,
+ filterMessage,
+ is_filter_dialog_visible,
+ toggleFilterDialog,
+}: TJournalToolsProps) => {
+ const toggle_ref = React.useRef(null);
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default JournalTools;
diff --git a/src/components/journal/journal.scss b/src/components/journal/journal.scss
new file mode 100644
index 00000000..0bac0519
--- /dev/null
+++ b/src/components/journal/journal.scss
@@ -0,0 +1,149 @@
+/**
+ * @define journal
+ */
+.journal {
+ &-empty {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ &__header {
+ padding: 0.8rem;
+ }
+ &__icon {
+ align-self: center;
+ }
+ &__message {
+ margin: 0 auto;
+ & .dc-text {
+ line-height: var(--text-lh-xxl);
+ }
+ }
+ &__list {
+ list-style-type: disc;
+ margin-left: 20px;
+ line-height: var(--text-lh-xl);
+
+ li::marker {
+ color: var(--text-less-prominent);
+ }
+ }
+ }
+ &__item {
+ padding: 16px;
+
+ &-list {
+ height: calc(100% - 7.2rem);
+ }
+ &-content {
+ > * {
+ width: 100%;
+ }
+ }
+ }
+ &__data-list {
+ .ReactVirtualized__Grid__innerScrollContainer {
+ > div:nth-child(even) {
+ background: var(--general-section-2);
+ }
+ }
+ }
+ &-tools {
+ &__container {
+ display: flex;
+ justify-content: space-between;
+ padding: 12px;
+ padding-left: 16px;
+ border: solid 1px var(--general-section-1);
+
+ &-filter {
+ @include flex-center(flex-end);
+ cursor: pointer;
+ flex: 1;
+
+ &--label {
+ margin-inline-end: 0.8rem;
+ }
+ }
+ }
+ }
+ &__text {
+ font-size: var(--text-size-xxs);
+ line-height: 1.5;
+ display: inline;
+ color: var(--text-general);
+
+ &-time,
+ &-date {
+ display: inline;
+ }
+ &-datetime {
+ color: var(--text-less-prominent);
+ font-size: var(--text-size--xxxs);
+ margin-top: 6px;
+ }
+ &--error {
+ color: var(--status-danger);
+ }
+ &--warn {
+ color: var(--status-warning);
+ }
+ &--info {
+ color: var(--status-info);
+ }
+ &--success {
+ color: var(--status-success);
+ }
+ &--bold {
+ font-weight: bold;
+ }
+ }
+ &__loader {
+ width: 350px;
+ height: 9.2rem;
+
+ &--mobile {
+ @extend .journal__loader;
+ width: 100vw;
+ }
+ }
+}
+
+.filter-dialog {
+ position: fixed;
+ display: grid;
+ grid-gap: 1.6rem;
+ background: var(--general-main-2);
+ border-radius: $BORDER_RADIUS;
+ box-shadow: 0 4px 16px 0 var(--shadow-menu);
+ transition:
+ transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
+ opacity 0.25s linear;
+ padding: 1.6rem 0.8rem;
+ padding-inline-end: 3.6rem;
+ inset-inline-end: 16px;
+ z-index: 99;
+
+ &--enter-done {
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+ }
+ &--enter,
+ &--exit {
+ opacity: 0;
+ transform: translate3d(0, -20px, 0);
+ }
+ &__input {
+ .input-wrapper__input {
+ border: 1px solid var(--border-normal);
+ }
+ }
+ &__button {
+ margin-top: 0.8rem;
+
+ .dc-btn {
+ width: 100%;
+ }
+ }
+}
diff --git a/src/components/journal/journal.tsx b/src/components/journal/journal.tsx
new file mode 100644
index 00000000..b432ef83
--- /dev/null
+++ b/src/components/journal/journal.tsx
@@ -0,0 +1,105 @@
+import classnames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { Text } from '@deriv-com/ui';
+
+import { contract_stages } from '@/constants/contract-stage';
+import { useStore } from '@/hooks/useStore';
+import { DataList, Icon, localize } from '@/utils/tmp/dummy';
+
+import { TCheckedFilters, TFilterMessageValues, TJournalDataListArgs } from './journal.types';
+import { JournalItem, JournalLoader, JournalTools } from './journal-components';
+
+const Journal = observer(() => {
+ const { ui } = useStore();
+ const { journal, run_panel } = useStore();
+ const {
+ checked_filters,
+ filterMessage,
+ filters,
+ filtered_messages,
+ is_filter_dialog_visible,
+ toggleFilterDialog,
+ unfiltered_messages,
+ } = journal;
+ const { is_stop_button_visible, contract_stage } = run_panel;
+
+ const filtered_messages_length = Array.isArray(filtered_messages) && filtered_messages.length;
+ const unfiltered_messages_length = Array.isArray(unfiltered_messages) && unfiltered_messages.length;
+ const { is_mobile } = ui;
+
+ return (
+
+
+
+ {filtered_messages_length ? (
+
}
+ keyMapper={(row: TFilterMessageValues) => row.unique_id}
+ />
+ ) : (
+ <>
+ {contract_stage >= contract_stages.STARTING &&
+ !!Object.keys(checked_filters as TCheckedFilters).length &&
+ !unfiltered_messages_length &&
+ is_stop_button_visible ? (
+
+ ) : (
+
+
+
+ {localize('There are no messages to display')}
+
+
+
+ {localize('Here are the possible reasons:')}
+
+
+
+
+ {localize('The bot is not running')}
+
+
+
+
+ {localize('The stats are cleared')}
+
+
+
+
+ {localize('All messages are filtered out')}
+
+
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+});
+
+export default Journal;
diff --git a/src/components/journal/journal.types.ts b/src/components/journal/journal.types.ts
new file mode 100644
index 00000000..383a9f93
--- /dev/null
+++ b/src/components/journal/journal.types.ts
@@ -0,0 +1,91 @@
+import React from 'react';
+
+type TExtraFilterMessage = {
+ currency: string;
+ profit: number;
+};
+type TExtraJournal = {
+ longcode: string;
+ transaction_id: number;
+};
+
+export type TDateItemProps = Record<'date' | 'time', string>;
+
+export type TFilterMessageValues = {
+ className: string;
+ date: string;
+ extra: TExtraFilterMessage | TExtraJournal;
+ message: string | ((value: () => void) => string);
+ message_type: string;
+ time: string;
+ unique_id: string;
+};
+
+type TFilterMessageProps = Array | TFilterMessageValues;
+
+type TKeyFilters = 'error' & 'notify' & 'success';
+type TValuesFilters = 'Errors' & 'Notifications' & 'System';
+
+type TFilters = Array<{ id: TKeyFilters; label: TValuesFilters }>;
+export type TCheckedFilters = Record<'error' | 'notify' | 'success', string[]>;
+
+export type TFilterDialogProps = {
+ toggle_ref: React.RefObject;
+ checked_filters: TCheckedFilters;
+ filters: TFilters;
+ filterMessage: () => TFilterMessageProps;
+ is_filter_dialog_visible: boolean;
+ toggleFilterDialog: () => void;
+};
+
+export type TJournalToolsProps = {
+ checked_filters: TCheckedFilters;
+ filters: TFilters;
+ filterMessage: () => TFilterMessageProps;
+ is_filter_dialog_visible: boolean;
+ toggleFilterDialog: () => void;
+};
+
+export type TFiltersProps = {
+ wrapper_ref: React.RefObject;
+ checked_filters: TCheckedFilters;
+ filters: TFilters;
+ filterMessage: (checked: boolean, item_id: number) => TFilterMessageProps;
+ className: string;
+ classNameLabel?: string;
+};
+
+export type TJournalProps = {
+ contract_stage: number;
+ filtered_messages: TFilterMessageProps | [];
+ is_drawer_open: boolean;
+ is_stop_button_visible: boolean;
+ unfiltered_messages: TFilterMessageProps;
+ checked_filters: TCheckedFilters;
+ filterMessage: () => TFilterMessageProps;
+ filters: TFilters;
+ is_filter_dialog_visible: boolean;
+ toggleFilterDialog: () => void;
+};
+
+export type TJournalItemProps = {
+ row: TFilterMessageValues;
+ is_new_row: boolean;
+ measure: () => void;
+};
+
+export type TJournalItemExtra = TExtraFilterMessage & TExtraJournal & { sold_for: string; current_currency?: string };
+
+export type TFormatMessageProps = {
+ logType: string;
+ className: string;
+ extra: TJournalItemExtra;
+};
+
+export type TJournalDataListArgs = {
+ is_new_row: boolean;
+ is_scrolling: boolean;
+ measure: () => void;
+ passthrough?: any;
+ row: TFilterMessageValues;
+};
diff --git a/src/components/layout/main-body/index.tsx b/src/components/layout/main-body/index.tsx
index 1ecfceae..95aad800 100644
--- a/src/components/layout/main-body/index.tsx
+++ b/src/components/layout/main-body/index.tsx
@@ -5,7 +5,7 @@ type TMainBodyProps = {
};
const MainBody: React.FC = ({ children }) => {
- return {children}
;
+ return {children}
;
};
export default MainBody;
diff --git a/src/components/layout/main-body/main-body.scss b/src/components/layout/main-body/main-body.scss
index 5a60180c..1999848e 100644
--- a/src/components/layout/main-body/main-body.scss
+++ b/src/components/layout/main-body/main-body.scss
@@ -1,4 +1,3 @@
.main-body {
flex-grow: 1;
- padding: 20px;
}
diff --git a/src/components/load-modal/index.ts b/src/components/load-modal/index.ts
new file mode 100644
index 00000000..6d97c835
--- /dev/null
+++ b/src/components/load-modal/index.ts
@@ -0,0 +1,5 @@
+import LoadModal from './load-modal';
+
+import './load-modal.scss';
+
+export default LoadModal;
diff --git a/src/components/load-modal/load-modal.scss b/src/components/load-modal/load-modal.scss
new file mode 100644
index 00000000..cbcafb81
--- /dev/null
+++ b/src/components/load-modal/load-modal.scss
@@ -0,0 +1,166 @@
+#modal_root {
+ .load-strategy {
+ &__recent {
+ display: flex;
+ gap: 1.6rem;
+
+ &__files {
+ width: 35%;
+ overflow: auto;
+ height: 100%;
+ margin: 0.2rem 0;
+ }
+
+ &__empty {
+ @include flex-center;
+ align-items: center;
+ flex-direction: column;
+
+ &-icon {
+ margin-bottom: 1.6rem;
+ }
+
+ &-title {
+ margin-bottom: 0.8rem;
+ font-size: var(--text-size-s);
+ font-weight: bold;
+ line-height: 2.4rem;
+ }
+
+ &-description {
+ margin-bottom: 1.6rem;
+ font-size: var(--text-size-xs);
+ line-height: 2rem;
+ }
+
+ &-expand {
+ margin-bottom: 0.8rem;
+ color: var(--brand-red-coral);
+ font-size: var(--text-size-xs);
+ font-weight: bold;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ &-explanation {
+ font-size: var(--text-size-xxs);
+ text-align: left;
+ opacity: 0;
+
+ &-list {
+ margin-top: 0.8rem;
+ }
+
+ &--show {
+ opacity: 1;
+ width: fit-content;
+ }
+ }
+ }
+
+ &-item {
+ grid-template-columns: 1fr 0.6fr;
+ position: relative;
+ display: grid;
+ grid-template-areas: ('text location');
+ padding: 1rem 0.8rem;
+ align-items: center;
+ text-align: center;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ &:not(:last-child) {
+ border-bottom: solid 1px var(--border-divider);
+ }
+
+ &--selected {
+ background-color: var(--general-section-2);
+ }
+
+ &-text {
+ height: unset;
+ flex-direction: column;
+ text-align: start;
+ padding-right: 0.8rem;
+ }
+
+ &-title {
+ font-size: var(--text-size-xs);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &-time {
+ font-size: var(--text-size-xxs);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ &-saved {
+ margin-inline-start: 1rem;
+ font-size: var(--text-size-xs);
+ line-height: 1.43;
+ word-break: break-word;
+ }
+
+ &-location {
+ @include flex-center(flex-start);
+ width: 100%;
+ word-break: break-word;
+ color: var(--text-general);
+ height: 100%;
+ }
+ }
+
+ &__preview {
+ width: 65%;
+ flex-basis: 65%;
+ display: flex;
+ flex-direction: column;
+
+ .load-strategy__preview-workspace {
+ height: calc(100% - 5.2rem);
+ min-height: unset;
+ margin: 0;
+ }
+
+ &-title {
+ margin: 1.5rem 0;
+ margin-left: 0;
+ }
+
+ &__title {
+ margin-left: 0;
+ }
+ }
+ }
+
+ &__container {
+ &--has-footer {
+ height: calc(80vh - 21rem);
+ margin-top: -1rem;
+ }
+ }
+
+ &__title {
+ margin: 1.5rem;
+ }
+ }
+
+ .load-strategy__preview-workspace {
+ min-height: unset;
+ height: unset;
+ margin: 0;
+ }
+}
diff --git a/src/components/load-modal/load-modal.tsx b/src/components/load-modal/load-modal.tsx
new file mode 100644
index 00000000..be0c7cf1
--- /dev/null
+++ b/src/components/load-modal/load-modal.tsx
@@ -0,0 +1,100 @@
+import { observer } from 'mobx-react-lite';
+
+import { Modal, Tabs } from '@deriv-com/ui';
+
+import { tabs_title } from '@/constants/load-modal';
+import { useStore } from '@/hooks/useStore';
+import { localize } from '@/utils/tmp/dummy';
+
+import GoogleDrive from '../../pages/dashboard/load-bot-preview/google-drive';
+import MobileFullPageModal from '../shared_ui/mobile-full-page-modal';
+
+import Local from './local';
+import LocalFooter from './local-footer';
+import Recent from './recent';
+import RecentFooter from './recent-footer';
+
+const LoadModal = observer(() => {
+ const { ui } = useStore();
+ const { load_modal, dashboard } = useStore();
+ const {
+ active_index,
+ is_load_modal_open,
+ loaded_local_file,
+ onEntered,
+ recent_strategies,
+ setActiveTabIndex,
+ toggleLoadModal,
+ tab_name,
+ } = load_modal;
+ const { setPreviewOnPopup } = dashboard;
+ const { is_mobile } = ui;
+ const header_text = localize('Load strategy');
+
+ if (is_mobile) {
+ return (
+ {
+ setPreviewOnPopup(false);
+ toggleLoadModal();
+ }}
+ height_offset='80px'
+ page_overlay
+ >
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const is_file_loaded = !!loaded_local_file && tab_name === tabs_title.TAB_LOCAL;
+ const has_recent_strategies = recent_strategies.length > 0 && tab_name === tabs_title.TAB_RECENT;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {has_recent_strategies && (
+
+
+
+ )}
+ {is_file_loaded && (
+
+
+
+ )}
+
+ );
+});
+
+export default LoadModal;
diff --git a/src/components/load-modal/local-footer.tsx b/src/components/load-modal/local-footer.tsx
new file mode 100644
index 00000000..63413351
--- /dev/null
+++ b/src/components/load-modal/local-footer.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+
+import { Button } from '@deriv-com/ui';
+
+import { NOTIFICATION_TYPE } from '@/components/bot-notification/bot-notification-utils';
+import { useStore } from '@/hooks/useStore';
+import { localize } from '@/utils/tmp/dummy';
+
+const LocalFooter = observer(() => {
+ const { ui } = useStore();
+ const { load_modal, dashboard } = useStore();
+ const { is_open_button_loading, loadFileFromLocal, setLoadedLocalFile, toggleLoadModal } = load_modal;
+ const { setOpenSettings, setPreviewOnPopup } = dashboard;
+
+ const { is_mobile } = ui;
+ const Wrapper = is_mobile ? Button.Group : React.Fragment;
+
+ return (
+
+ {is_mobile && (
+ setLoadedLocalFile(null)} has_effect secondary large />
+ )}
+ {
+ loadFileFromLocal();
+ toggleLoadModal();
+ setPreviewOnPopup(false);
+ setOpenSettings(NOTIFICATION_TYPE.BOT_IMPORT);
+ }}
+ is_loading={is_open_button_loading}
+ has_effect
+ primary
+ large
+ />
+
+ );
+});
+
+export default LocalFooter;
diff --git a/src/components/load-modal/local.tsx b/src/components/load-modal/local.tsx
new file mode 100644
index 00000000..74e7203b
--- /dev/null
+++ b/src/components/load-modal/local.tsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { Button } from '@deriv-com/ui';
+
+import { useStore } from '@/hooks/useStore';
+import { Icon, Localize, localize } from '@/utils/tmp/dummy';
+
+import LocalFooter from './local-footer';
+import WorkspaceControl from './workspace-control';
+
+const LocalComponent = observer(() => {
+ const { ui } = useStore();
+ const { dashboard, load_modal } = useStore();
+ const { active_tab, active_tour } = dashboard;
+ const { handleFileChange, loaded_local_file, setLoadedLocalFile } = load_modal;
+
+ const file_input_ref = React.useRef(null);
+ const [is_file_supported, setIsFileSupported] = React.useState(true);
+ const { is_mobile } = ui;
+
+ if (loaded_local_file && is_file_supported) {
+ return (
+
+
+
+
+
+
+
+
+ setLoadedLocalFile(null)}
+ />
+
+
+
+
+
+ {is_mobile && (
+
+
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+
setIsFileSupported(handleFileChange(e, false))}
+ data-testid='dt-load-strategy-file-input'
+ />
+
{
+ handleFileChange(e, false);
+ }}
+ >
+ {is_mobile ? (
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
+
file_input_ref?.current?.click()}
+ has_effect
+ primary
+ large
+ />
+
+
+
+ );
+});
+
+export default LocalComponent;
diff --git a/src/components/load-modal/recent-footer.tsx b/src/components/load-modal/recent-footer.tsx
new file mode 100644
index 00000000..9d5cade2
--- /dev/null
+++ b/src/components/load-modal/recent-footer.tsx
@@ -0,0 +1,30 @@
+import { observer } from 'mobx-react-lite';
+
+import { Button } from '@deriv-com/ui';
+
+import { NOTIFICATION_TYPE } from '@/components/bot-notification/bot-notification-utils';
+import { useStore } from '@/hooks/useStore';
+import { localize } from '@/utils/tmp/dummy';
+
+const RecentFooter = observer(() => {
+ const { load_modal, dashboard } = useStore();
+ const { is_open_button_loading, loadFileFromRecent, toggleLoadModal } = load_modal;
+ const { setOpenSettings } = dashboard;
+
+ return (
+ {
+ loadFileFromRecent();
+ toggleLoadModal();
+ setOpenSettings(NOTIFICATION_TYPE.BOT_IMPORT);
+ }}
+ is_loading={is_open_button_loading}
+ has_effect
+ primary
+ large
+ />
+ );
+});
+
+export default RecentFooter;
diff --git a/src/components/load-modal/recent-workspace.tsx b/src/components/load-modal/recent-workspace.tsx
new file mode 100644
index 00000000..5388a009
--- /dev/null
+++ b/src/components/load-modal/recent-workspace.tsx
@@ -0,0 +1,45 @@
+import classnames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { timeSince } from '@/external/bot-skeleton';
+import { save_types } from '@/external/bot-skeleton/constants/save-type';
+import { useStore } from '@/hooks/useStore';
+import { Icon } from '@/utils/tmp/dummy';
+
+type TRecentWorkspaceProps = {
+ workspace: { [key: string]: any };
+};
+
+const RecentWorkspace = observer(({ workspace }: TRecentWorkspaceProps) => {
+ const { load_modal } = useStore();
+ const { getRecentFileIcon, getSaveType, previewRecentStrategy, selected_strategy_id } = load_modal;
+
+ return (
+ previewRecentStrategy(workspace.id)}
+ data-testid='dt_recent_workspace_item'
+ >
+
+
+ {workspace.name}
+
+
{timeSince(workspace.timestamp)}
+
+
+
+
{getSaveType(workspace.save_type)}
+
+
+ );
+});
+
+export default RecentWorkspace;
diff --git a/src/components/load-modal/recent.tsx b/src/components/load-modal/recent.tsx
new file mode 100644
index 00000000..acc456cb
--- /dev/null
+++ b/src/components/load-modal/recent.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import classnames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { useStore } from '@/hooks/useStore';
+import { Icon, Localize } from '@/utils/tmp/dummy';
+
+import RecentWorkspace from './recent-workspace';
+import WorkspaceControl from './workspace-control';
+
+const RecentComponent = observer(() => {
+ const { load_modal } = useStore();
+ const { is_explanation_expand, recent_strategies, toggleExplanationExpand } = load_modal;
+ if (recent_strategies.length) {
+ return (
+
+
+
+ {recent_strategies.map(workspace => (
+
+ ))}
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
{
+ if (e.key === 'Enter') toggleExplanationExpand();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+export default RecentComponent;
diff --git a/src/components/load-modal/workspace-control.tsx b/src/components/load-modal/workspace-control.tsx
new file mode 100644
index 00000000..3e578fe0
--- /dev/null
+++ b/src/components/load-modal/workspace-control.tsx
@@ -0,0 +1,36 @@
+import { observer } from 'mobx-react-lite';
+
+import { useStore } from '@/hooks/useStore';
+import { Icon } from '@/utils/tmp/dummy';
+
+type TWorkspaceControlProps = {
+ mockZoomInOut?: (is_zoom_in: boolean) => void;
+};
+
+const WorkspaceControl = observer(({ mockZoomInOut }: TWorkspaceControlProps) => {
+ const { dashboard } = useStore();
+ const { onZoomInOutClick } = dashboard;
+
+ return (
+
+ {
+ mockZoomInOut ? mockZoomInOut(true) : onZoomInOutClick(true);
+ }}
+ data_testid='zoom-in'
+ />
+ {
+ mockZoomInOut ? mockZoomInOut(false) : onZoomInOutClick(false);
+ }}
+ data_testid='zoom-out'
+ />
+
+ );
+});
+
+export default WorkspaceControl;
diff --git a/src/components/network-toast-popup/index.ts b/src/components/network-toast-popup/index.ts
new file mode 100644
index 00000000..f5da6924
--- /dev/null
+++ b/src/components/network-toast-popup/index.ts
@@ -0,0 +1,3 @@
+import NetworkToastPopup from './network-toast-popup';
+
+export default NetworkToastPopup;
diff --git a/src/components/network-toast-popup/network-toast-popup.tsx b/src/components/network-toast-popup/network-toast-popup.tsx
new file mode 100644
index 00000000..ff9b61ed
--- /dev/null
+++ b/src/components/network-toast-popup/network-toast-popup.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+// import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+// import { Toast } from '@deriv-com/ui';
+// import { useStore } from '@/hooks/useStore';
+
+// TODO: Need to sanitize,
+// Same sort of component is being used inside DTrader,
+// In Future we might move the `NetworkStatusToastError` to the core packages for resuability.
+
+/**
+ * Network status Toast components
+ */
+
+const NetworkStatusToastError = observer(({ should_open = false }: { should_open?: boolean }) => {
+ // const { common } = useStore();
+ // const { network_status } = common;
+ const [is_open, setIsOpen] = React.useState(should_open);
+ // const { message, status } = network_status;
+ const portal_el = document.getElementById('popup_root');
+
+ // if (!portal_el || !message) return null;
+ if (!portal_el) return null;
+
+ if (!is_open && status !== 'online') {
+ setIsOpen(true); // open if status === 'blinker' or 'offline'
+ } else if (is_open && status === 'online') {
+ setTimeout(() => {
+ setIsOpen(false);
+ }, 1500);
+ }
+
+ // TODO: fix
+ return ReactDOM.createPortal(
+ //
+ //
+ // {message}
+ //
+ // ,
+ toast
,
+ portal_el
+ );
+});
+
+export default NetworkStatusToastError;
diff --git a/src/components/notify-item/index.ts b/src/components/notify-item/index.ts
new file mode 100644
index 00000000..8be7a130
--- /dev/null
+++ b/src/components/notify-item/index.ts
@@ -0,0 +1,5 @@
+import { arrayAsMessage, messageWithButton, messageWithImage } from './notify-item';
+
+import './notify-item.scss';
+
+export { arrayAsMessage, messageWithButton, messageWithImage };
diff --git a/src/components/notify-item/notify-item.scss b/src/components/notify-item/notify-item.scss
new file mode 100644
index 00000000..0de54d85
--- /dev/null
+++ b/src/components/notify-item/notify-item.scss
@@ -0,0 +1,23 @@
+.notify {
+ &__item {
+ &-button {
+ margin-top: 8px;
+ height: 2.8rem !important;
+ margin-left: 100%;
+ transform: translateX(-100%);
+
+ .btn__text {
+ font-size: var(--text-size-xxs);
+ }
+ }
+ &-container {
+ display: flex;
+ align-items: center;
+ }
+ &-message {
+ margin-left: 8px;
+ font-size: var(--text-size-xxs);
+ line-height: 1.2;
+ }
+ }
+}
diff --git a/src/components/notify-item/notify-item.tsx b/src/components/notify-item/notify-item.tsx
new file mode 100644
index 00000000..74c05233
--- /dev/null
+++ b/src/components/notify-item/notify-item.tsx
@@ -0,0 +1,55 @@
+import { Button } from '@deriv-com/ui';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import ExpansionPanel from '../shared_ui/expansion-panel';
+
+export const getIcon = (type: string) => {
+ switch (type) {
+ case 'error':
+ return 'IcAlertDanger';
+ case 'warn':
+ return 'IcAlertWarning';
+ case 'info':
+ return 'IcAlertInfo';
+ default:
+ return 'IcAlertWarning';
+ }
+};
+
+type TMessageWithButton = {
+ unique_id: string;
+ type: string;
+ message: string;
+ btn_text: string;
+ onClick: () => void;
+};
+
+export const messageWithButton = ({ unique_id, type, message, btn_text, onClick }: TMessageWithButton) => (
+ <>
+
+
+ >
+);
+
+export const messageWithImage = (message: string, image: string) => (
+ <>
+ {message}
+
+ >
+);
+
+// eslint-disable-next-line react/display-name
+export const arrayAsMessage = parsedArray => measure => ;
diff --git a/src/components/route-prompt-dialog/index.ts b/src/components/route-prompt-dialog/index.ts
new file mode 100644
index 00000000..a506ef60
--- /dev/null
+++ b/src/components/route-prompt-dialog/index.ts
@@ -0,0 +1,3 @@
+import RoutePromptDialog from './route-prompt-dialog';
+
+export default RoutePromptDialog;
diff --git a/src/components/route-prompt-dialog/route-prompt-dialog.tsx b/src/components/route-prompt-dialog/route-prompt-dialog.tsx
new file mode 100644
index 00000000..ae00d4cb
--- /dev/null
+++ b/src/components/route-prompt-dialog/route-prompt-dialog.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import { observer } from 'mobx-react-lite';
+
+import { Dialog } from '@deriv-com/ui';
+
+import { useStore } from '@/hooks/useStore';
+import { Localize, localize } from '@/utils/tmp/dummy';
+
+const RoutePromptDialog = observer(() => {
+ const { route_prompt_dialog } = useStore();
+ const { continueRoute, should_show, is_confirmed, last_location, onCancel, onConfirm } = route_prompt_dialog;
+
+ React.useEffect(continueRoute, [is_confirmed, last_location, continueRoute]);
+
+ return (
+
+
+
+ );
+});
+
+export default RoutePromptDialog;
diff --git a/src/components/run-panel/index.ts b/src/components/run-panel/index.ts
new file mode 100644
index 00000000..97cba3d6
--- /dev/null
+++ b/src/components/run-panel/index.ts
@@ -0,0 +1,5 @@
+import RunPanel from './run-panel';
+
+import './run-panel.scss';
+
+export default RunPanel;
diff --git a/src/components/run-panel/run-panel.scss b/src/components/run-panel/run-panel.scss
new file mode 100644
index 00000000..ad76334f
--- /dev/null
+++ b/src/components/run-panel/run-panel.scss
@@ -0,0 +1,392 @@
+/**
+* @define -panel
+**/
+// TODO: [fix-dc-bundle] Fix import issue with Deriv Component stylesheets (app should take precedence, and not repeat)
+.run-panel {
+ height: 0;
+
+ &__container {
+ height: var(--bot-content-height) !important;
+ top: 10.4rem !important;
+ width: 36.6rem !important;
+ right: 0;
+
+ &--mobile {
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ z-index: var(--zindex-drawer);
+
+ &-closed {
+ position: unset;
+ }
+ }
+ }
+
+ &__tile {
+ @include flex-center;
+
+ flex-direction: column;
+ height: 100%;
+
+ &-title {
+ min-height: 1.8rem;
+ margin-bottom: 4px;
+
+ @include typeface(--small-center-bold-black, none);
+ }
+
+ &-content {
+ height: 18px;
+ margin-bottom: 4px;
+
+ @include typeface(--small-center-normal-black, none);
+ }
+ }
+
+ &__stat {
+ @include flex-center(flex-start, flex-end);
+
+ flex-direction: column;
+ width: 35rem;
+ background-color: var(--general-section-2);
+
+ @include mobile {
+ margin: 0;
+ position: fixed;
+ }
+
+ &--info {
+ @include flex-center(center, flex-start);
+
+ width: 33%;
+ padding: 16px 0 2px;
+ cursor: pointer;
+ color: var(--text-general);
+
+ &-item {
+ display: inline-block;
+ border-bottom: 1px dotted var(--text-general);
+ }
+ }
+
+ &--tiles {
+ width: 100%;
+ display: grid;
+ grid-template: 64px 64px / 1fr 1fr 1fr;
+ }
+
+ &-amount {
+ @include typeface(--small-center-normal-black, none);
+
+ &--positive {
+ font-weight: bold;
+ color: var(--text-profit-success);
+ }
+
+ &--negative {
+ font-weight: bold;
+ color: var(--text-loss-danger);
+ }
+ }
+
+ &--mobile {
+ width: 100%;
+ bottom: 0;
+ }
+ }
+
+ &__footer {
+ text-align: center;
+
+ @include flex-center(space-between);
+
+ flex-direction: column;
+
+ &-button {
+ width: 318px;
+
+ // TODO: [fix-dc-bundle] Fix import issue with Deriv Component stylesheets (app should take precedence, and not repeat)
+ height: 32px !important;
+ margin: 12px 24px;
+ }
+ }
+
+ &__content {
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+ overflow: hidden !important;
+ }
+
+ &__buttons {
+ display: inline-flex !important;
+ justify-content: space-between;
+ width: 67%;
+ margin-top: 1rem;
+ align-items: center;
+ }
+
+ &-tab {
+ &__content {
+ height: calc(100vh - 42rem);
+ overflow: hidden;
+
+ &--no-stat {
+ height: var(--drawer-content-height-no-stat);
+ }
+
+ &--mobile {
+ display: flex;
+ height: var(--drawer-content-height-mobile);
+ position: fixed;
+ bottom: 15.7rem;
+ width: 100vw;
+ padding: 0.4rem 0;
+ }
+
+ &--summary-tab {
+ padding: 0.8rem 1.6rem;
+ }
+ }
+ }
+
+ &__clear-button {
+ @include mobile {
+ position: absolute !important;
+ top: 0.5rem;
+ right: 1.6rem;
+ height: 2.6rem !important;
+ min-width: 8rem;
+ }
+ }
+}
+
+.controls {
+ &__section {
+ @include flex-center(space-between);
+
+ flex-direction: column;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ background-color: var(--general-main-1);
+ border-top: solid 2px var(--general-section-1);
+ z-index: 8;
+ }
+
+ &__buttons {
+ padding: 0.8rem 2.4rem;
+ height: 6rem;
+ display: flex;
+ width: inherit;
+ }
+
+ &__stop-button,
+ &__run-button {
+ width: 40%;
+ border-radius: 4px 0 0 4px !important;
+ }
+
+ &__animation {
+ width: 100%;
+ height: 4rem;
+ border-radius: 0 4px 4px 0;
+ }
+}
+
+// animation
+.list {
+ &__animation {
+ &-enter {
+ height: 0;
+ transform: translateX(200%);
+
+ &-active {
+ height: auto;
+ transform: translateX(0%);
+ transition:
+ height 500ms,
+ transform 500ms;
+ }
+ }
+
+ &-exit {
+ opacity: 1;
+
+ &-active {
+ opacity: 0;
+ transition: opacity 300ms;
+ }
+ }
+ }
+}
+
+.db-self-exclusion {
+ font-size: var(--text-size-xs);
+ font-weight: normal;
+ line-height: 1.43;
+ color: var(--text-general);
+
+ &__content {
+ margin: 2.4rem;
+ margin-right: 1.4rem;
+ padding-right: 1rem;
+ }
+
+ & .dc-themed-scrollbars__track--vertical {
+ right: -0.2rem;
+ }
+
+ // TODO: [fix-dc-bundle] Fix import issue with Deriv Component stylesheets (app should take precedence, and not repeat)
+ &__dialog {
+ max-height: 500px !important;
+ width: 460px;
+ }
+
+ &__footer {
+ height: 72px;
+ display: block;
+ position: fixed;
+ width: 100%;
+ left: 0;
+ bottom: 0;
+ padding: 1.4rem;
+ border-top: 2px solid var(--general-section-2);
+
+ @include mobile {
+ position: absolute;
+ width: 100%;
+ bottom: 40px;
+ left: 0;
+
+ &--relative {
+ position: relative;
+ margin-top: 5rem;
+ }
+ }
+
+ &-btn-group {
+ display: flex;
+ justify-content: flex-end;
+ }
+ }
+
+ &__info {
+ margin-bottom: 2rem;
+ }
+
+ &__limit-status {
+ margin: 1rem 0;
+
+ &--bold {
+ font-weight: 700;
+ }
+
+ &--danger {
+ font-weight: 700;
+ color: var(--status-danger);
+ }
+ }
+
+ &--danger {
+ color: var(--status-danger);
+ }
+
+ & .dc-btn {
+ margin-left: 0.8rem;
+ }
+}
+
+.limits__wrapper {
+ @include mobile {
+ position: fixed;
+ z-index: 5;
+ width: 100%;
+ left: 0;
+ top: 0;
+ background: var(--general-main-1);
+
+ .db-self-exclusion {
+ height: calc(100vh - 40px);
+ display: flex;
+ justify-content: space-between;
+ flex-direction: column;
+
+ &__content {
+ margin-top: 56px;
+ }
+
+ &__form-group {
+ margin-bottom: 1.6rem;
+ display: flex;
+ }
+ }
+ }
+}
+
+.dc-modal__container_self-exclusion__modal {
+ display: flex;
+ flex-direction: row;
+ align-content: space-between;
+ width: 440px !important;
+ height: 374px !important;
+}
+
+.statistics {
+ &__modal {
+ height: 28.4rem !important;
+ width: 44rem !important;
+ font-size: 1.6rem;
+ padding: 2.4rem;
+
+ &--mobile {
+ font-size: 1.6rem !important;
+ }
+
+ &-body {
+ height: calc(100vh - 40.6rem);
+ min-height: 10rem;
+ max-height: 45rem;
+
+ &--mobile {
+ padding: 0.6rem 0 1.6rem !important;
+ height: 40.4rem;
+ }
+
+ &--content {
+ margin-top: 1rem;
+
+ &-stake {
+ margin-top: unset;
+ font-weight: bold;
+ }
+ }
+ }
+
+ &-scrollbar {
+ padding-right: 1.2rem;
+ }
+ }
+}
+
+.dc-modal__container_statistics__modal {
+ @include mobile {
+ width: 31.2rem !important;
+ }
+
+ .dc-modal-body {
+ padding: 2.4rem 1.2rem 2.4rem 2.4rem;
+ }
+}
+
+.dc-dialog {
+ &__button {
+ @include mobile {
+ flex-basis: 100%;
+ margin-left: unset;
+ }
+ }
+}
diff --git a/src/components/run-panel/run-panel.tsx b/src/components/run-panel/run-panel.tsx
new file mode 100644
index 00000000..e452b17c
--- /dev/null
+++ b/src/components/run-panel/run-panel.tsx
@@ -0,0 +1,333 @@
+import React from 'react';
+import classNames from 'classnames';
+import { observer } from 'mobx-react-lite';
+
+import { Button, Modal, Tabs, Text } from '@deriv-com/ui';
+
+import Journal from '@/components/journal';
+import SelfExclusion from '@/components/self-exclusion';
+import Drawer from '@/components/shared_ui/drawer';
+import Money from '@/components/shared_ui/money';
+import Summary from '@/components/summary';
+import TradeAnimation from '@/components/trade-animation';
+import Transactions from '@/components/transactions';
+import { DBOT_TABS } from '@/constants/bot-contents';
+import { popover_zindex } from '@/constants/z-indexes';
+import { useStore } from '@/hooks/useStore';
+import { Localize, localize } from '@/utils/tmp/dummy';
+
+import ThemedScrollbars from '../shared_ui/themed-scrollbars';
+
+type TStatisticsTile = {
+ content: React.ElementType | string;
+ contentClassName: string;
+ title: string;
+};
+
+type TStatisticsSummary = {
+ currency: string;
+ is_mobile: boolean;
+ lost_contracts: number;
+ number_of_runs: number;
+ total_stake: number;
+ total_payout: number;
+ toggleStatisticsInfoModal: () => void;
+ total_profit: number;
+ won_contracts: number;
+};
+type TDrawerHeader = {
+ is_clear_stat_disabled: boolean;
+ is_mobile: boolean;
+ is_drawer_open: boolean;
+ onClearStatClick: () => void;
+};
+
+type TDrawerContent = {
+ active_index: number;
+ is_drawer_open: boolean;
+ active_tour: string;
+ setActiveTabIndex: () => void;
+};
+
+type TDrawerFooter = {
+ is_clear_stat_disabled: boolean;
+ onClearStatClick: () => void;
+};
+
+type TStatisticsInfoModal = {
+ is_mobile: boolean;
+ is_statistics_info_modal_open: boolean;
+ toggleStatisticsInfoModal: () => void;
+};
+
+const StatisticsTile = ({ content, contentClassName, title }: TStatisticsTile) => (
+
+);
+
+export const StatisticsSummary = ({
+ currency,
+ is_mobile,
+ lost_contracts,
+ number_of_runs,
+ total_stake,
+ total_payout,
+ toggleStatisticsInfoModal,
+ total_profit,
+ won_contracts,
+}: TStatisticsSummary) => (
+
+
+
+ }
+ />
+ }
+ />
+
+
+
+ }
+ alignment='bottom'
+ contentClassName={classNames('run-panel__stat-amount', {
+ 'run-panel__stat-amount--positive': total_profit > 0,
+ 'run-panel__stat-amount--negative': total_profit < 0,
+ })}
+ />
+
+
+);
+
+const DrawerHeader = ({ is_clear_stat_disabled, is_mobile, is_drawer_open, onClearStatClick }: TDrawerHeader) =>
+ is_mobile &&
+ is_drawer_open && (
+
+ );
+
+const DrawerContent = ({ active_index, is_drawer_open, active_tour, setActiveTabIndex, ...props }: TDrawerContent) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+ {((is_drawer_open && active_index !== 2) || active_tour) && }
+ >
+ );
+};
+
+const DrawerFooter = ({ is_clear_stat_disabled, onClearStatClick }: TDrawerFooter) => (
+
+
+
+);
+
+const MobileDrawerFooter = () => {
+ return (
+
+ );
+};
+
+const StatisticsInfoModal = ({
+ is_mobile,
+ is_statistics_info_modal_open,
+ toggleStatisticsInfoModal,
+}: TStatisticsInfoModal) => {
+ return (
+
+
+
+
+
+ {localize('Total stake')}
+
+ {localize('Total stake since you last cleared your stats.')}
+
+ {localize('Total payout')}
+
+ {localize('Total payout since you last cleared your stats.')}
+
+ {localize('No. of runs')}
+
+
+ {localize(
+ 'The number of times your bot has run since you last cleared your stats. Each run includes the execution of all the root blocks.'
+ )}
+
+
+ {localize('Contracts lost')}
+
+
+ {localize('The number of contracts you have lost since you last cleared your stats.')}
+
+
+ {localize('Contracts won')}
+
+
+ {localize('The number of contracts you have won since you last cleared your stats.')}
+
+
+ {localize('Total profit/loss')}
+
+
+ {localize(
+ 'Your total profit/loss since you last cleared your stats. It is the difference between your total payout and your total stake.'
+ )}
+
+
+
+
+
+ );
+};
+
+const RunPanel = observer(() => {
+ const { run_panel, dashboard, transactions } = useStore();
+ const {
+ client,
+ ui: { is_mobile },
+ } = useStore();
+ const { currency } = client;
+ const {
+ active_index,
+ is_drawer_open,
+ is_statistics_info_modal_open,
+ is_clear_stat_disabled,
+ onClearStatClick,
+ onMount,
+ onRunButtonClick,
+ onUnmount,
+ setActiveTabIndex,
+ toggleDrawer,
+ toggleStatisticsInfoModal,
+ } = run_panel;
+ const { statistics } = transactions;
+ const { active_tour, active_tab } = dashboard;
+ const { total_payout, total_profit, total_stake, won_contracts, lost_contracts, number_of_runs } = statistics;
+ const { BOT_BUILDER, CHART } = DBOT_TABS;
+
+ React.useEffect(() => {
+ onMount();
+ return () => onUnmount();
+ }, [onMount, onUnmount]);
+
+ React.useEffect(() => {
+ if (is_mobile) {
+ toggleDrawer(false);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const content = (
+
+ );
+
+ const footer = ;
+
+ const header = (
+
+ );
+
+ const show_run_panel = [BOT_BUILDER, CHART].includes(active_tab) || active_tour;
+ if ((!show_run_panel && !is_mobile) || active_tour === 'bot_builder') return null;
+
+ return (
+ <>
+
+
+ {content}
+
+ {is_mobile && }
+
+
+
+ >
+ );
+});
+
+export default RunPanel;
diff --git a/src/components/self-exclusion/index.ts b/src/components/self-exclusion/index.ts
new file mode 100644
index 00000000..7683ab59
--- /dev/null
+++ b/src/components/self-exclusion/index.ts
@@ -0,0 +1,3 @@
+import SelfExclusion from './self-exclusion';
+
+export default SelfExclusion;
diff --git a/src/components/self-exclusion/self-exclusion.jsx b/src/components/self-exclusion/self-exclusion.jsx
new file mode 100644
index 00000000..07ba7b45
--- /dev/null
+++ b/src/components/self-exclusion/self-exclusion.jsx
@@ -0,0 +1,251 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Field, Form, Formik } from 'formik';
+import { observer } from 'mobx-react-lite';
+import PropTypes from 'prop-types';
+
+import { Button, Input, Modal } from '@deriv-com/ui';
+
+import { useStore } from '@/hooks/useStore';
+import { localize } from '@/utils/tmp/dummy';
+
+import Div100vhContainer from '../shared_ui/div100vh-container';
+import FadeWrapper from '../shared_ui/fade-wrapper';
+import MobileWrapper from '../shared_ui/mobile-wrapper';
+import PageOverlay from '../shared_ui/page-overlay';
+
+const SelfExclusionForm = props => {
+ const [max_losses_error, setMaxLossesError] = React.useState('');
+ const {
+ is_onscreen_keyboard_active,
+ is_logged_in,
+ initial_values,
+ api_max_losses,
+ onRunButtonClick,
+ resetSelfExclusion,
+ updateSelfExclusion,
+ setRunLimit,
+ is_mobile,
+ } = props;
+
+ React.useEffect(() => {
+ if (!is_logged_in) {
+ resetSelfExclusion();
+ }
+ });
+
+ const onSubmitLimits = async values => {
+ if (values.form_max_losses !== api_max_losses) {
+ const set_losses = await updateSelfExclusion({ max_losses: values.form_max_losses });
+ if (set_losses?.error) {
+ setMaxLossesError(localize(set_losses.error.message));
+ return;
+ }
+ }
+ setRunLimit(values.run_limit);
+ onRunButtonClick();
+ };
+ const validateFields = values => {
+ const errors = {};
+ // Regex
+ const is_number = /^\d+(\.\d+)?$/;
+ const is_integer = /^\d+$/;
+ const max_number = 9999999999999;
+
+ // Messages
+ const requested_field_message = localize('This field is required.');
+ const valid_number_message = localize('Should be a valid number');
+ const max_number_message = localize('Reached maximum number of digits');
+ const max_decimal_message = localize('Reached maximum number of decimals');
+ const max_losses_limit_message = localize('Please enter a number between 0 and {{api_max_losses}}.', {
+ api_max_losses,
+ });
+ const requested_fields = ['run_limit', 'form_max_losses'];
+ const only_numbers = ['run_limit', 'form_max_losses'];
+ const decimal_limit = ['form_max_losses'];
+ const has_max_limit = ['form_max_losses'];
+
+ const only_integers = ['run_limit'];
+
+ requested_fields.forEach(item => {
+ if (!values[item]) {
+ errors[item] = requested_field_message;
+ }
+ });
+
+ only_numbers.forEach(item => {
+ if (values[item]) {
+ if (!is_number.test(values[item])) {
+ errors[item] = valid_number_message;
+ } else if (+values[item] > max_number) {
+ errors[item] = max_number_message;
+ }
+ }
+ });
+
+ only_integers.forEach(item => {
+ if (values[item]) {
+ if (!is_integer.test(values[item])) {
+ errors[item] = valid_number_message;
+ }
+ }
+ });
+
+ decimal_limit.forEach(item => {
+ const amount_decimal_array = values[item].toString().split('.')[1];
+ const amount_decimal_places = amount_decimal_array ? amount_decimal_array.length || 0 : 0;
+ if (amount_decimal_places > 2) {
+ errors[item] = max_decimal_message;
+ }
+ });
+ has_max_limit.forEach(item => {
+ if (api_max_losses !== 0 && api_max_losses !== values[item] && api_max_losses < values[item]) {
+ errors[item] = max_losses_limit_message;
+ } else {
+ setMaxLossesError('');
+ }
+ });
+
+ return errors;
+ };
+
+ return (
+
+
+
+ {localize('Enter limits to stop your bot from trading when any of these conditions are met.')}
+
+
+ {({ values, touched, errors, isValid, handleChange }) => {
+ return (
+
+ );
+ }}
+
+
+
+ );
+};
+
+const SelfExclusion = observer(({ onRunButtonClick }) => {
+ const { self_exclusion } = useStore();
+ const { ui, client } = useStore();
+ const { is_restricted, resetSelfExclusion, initial_values, api_max_losses, run_limit, setRunLimit } =
+ self_exclusion;
+ const { is_onscreen_keyboard_active, is_mobile } = ui;
+ const { is_logged_in, updateSelfExclusion, virtual_account_loginid } = client;
+
+ const self_exclusion_form_props = {
+ is_onscreen_keyboard_active,
+ is_logged_in,
+ initial_values,
+ api_max_losses,
+ onRunButtonClick,
+ resetSelfExclusion,
+ updateSelfExclusion,
+ setRunLimit,
+ virtual_account_loginid,
+ run_limit,
+ is_mobile,
+ };
+
+ return (
+ <>
+ {is_mobile ? (
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+ >
+ );
+});
+
+SelfExclusion.propTypes = {
+ onRunButtonClick: PropTypes.func,
+};
+
+export default SelfExclusion;
diff --git a/src/components/shared/brand.config.json b/src/components/shared/brand.config.json
new file mode 100644
index 00000000..2627ac49
--- /dev/null
+++ b/src/components/shared/brand.config.json
@@ -0,0 +1,79 @@
+{
+ "brand_name": "Deriv",
+ "domain_name": "Deriv.com",
+ "legal_entities": {
+ "fx": "Deriv (FX) Ltd",
+ "malta": "Deriv (Europe) Limited",
+ "maltainvest": "Deriv Investments (Europe) Limited",
+ "mx": "Deriv (MX) Ltd",
+ "samoa": "Deriv Capital International Ltd",
+ "svg": "Deriv (SVG) LLC",
+ "v": "Deriv (V) Ltd"
+ },
+ "platforms": {
+ "trader": {
+ "name": "Deriv Trader",
+ "icon": "IcRebrandingDerivTrader"
+ },
+ "dbot": {
+ "name": "Deriv Bot",
+ "icon": "IcRebrandingDerivBot"
+ },
+ "mt5": {
+ "name": "Deriv MT5",
+ "icon": "IcRebrandingDmt5"
+ },
+ "ctrader": {
+ "name": "Deriv cTrader",
+ "icon": "IcRebrandingCtrader"
+ },
+ "dxtrade": {
+ "name": "Deriv X",
+ "icon": "IcRebrandingDxtrade"
+ },
+ "smarttrader": {
+ "name": "SmartTrader",
+ "icon": "IcRebrandingSmarttrader"
+ },
+ "bbot": {
+ "name": "Binary Bot",
+ "icon": "IcRebrandingBinaryBot"
+ },
+ "go": {
+ "name": "Deriv GO",
+ "icon": "IcRebrandingDerivGo"
+ }
+ },
+ "platforms_appstore": {
+ "trader": {
+ "name": "Deriv Trader",
+ "icon": "DTrader",
+ "availability": "All"
+ },
+ "dbot": {
+ "name": "Deriv Bot",
+ "icon": "DBot",
+ "availability": "Non-EU"
+ },
+ "smarttrader": {
+ "name": "SmartTrader",
+ "icon": "SmartTrader",
+ "availability": "Non-EU"
+ },
+ "bbot": {
+ "name": "Binary Bot",
+ "icon": "BinaryBot",
+ "availability": "Non-EU"
+ },
+ "go": {
+ "name": "Deriv GO",
+ "icon": "DerivGo",
+ "availability": "Non-EU"
+ },
+ "ctrader": {
+ "name": "",
+ "icon": "",
+ "availability": ""
+ }
+ }
+}
diff --git a/src/components/shared/helpers/context/poi-context.tsx b/src/components/shared/helpers/context/poi-context.tsx
new file mode 100644
index 00000000..fae7cc4a
--- /dev/null
+++ b/src/components/shared/helpers/context/poi-context.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { ResidenceList } from '@deriv/api-types';
+
+const submission_status_code = {
+ selecting: 'selecting',
+ submitting: 'submitting',
+ complete: 'complete',
+} as const;
+
+const service_code = {
+ idv: 'idv',
+ onfido: 'onfido',
+ manual: 'manual',
+} as const;
+
+type TSubmissionStatus = keyof typeof submission_status_code;
+type TSubmissionService = keyof typeof service_code;
+
+type TPOIContext = {
+ submission_status: TSubmissionStatus;
+ setSubmissionStatus: React.Dispatch>;
+ submission_service: TSubmissionService;
+ setSubmissionService: React.Dispatch>;
+ selected_country: ResidenceList[number];
+ setSelectedCountry: React.Dispatch>;
+};
+
+export const POIContextInitialState: TPOIContext = {
+ submission_status: submission_status_code.selecting,
+ setSubmissionStatus: () => submission_status_code.selecting,
+ submission_service: service_code.idv,
+ setSubmissionService: () => service_code.idv,
+ selected_country: {},
+ setSelectedCountry: () => ({}),
+};
+
+export const POIContext = React.createContext(POIContextInitialState);
+
+export const POIProvider = ({ children }: React.PropsWithChildren) => {
+ const [submission_status, setSubmissionStatus] = React.useState(
+ submission_status_code.selecting
+ );
+ const [submission_service, setSubmissionService] = React.useState(service_code.idv);
+ const [selected_country, setSelectedCountry] = React.useState({});
+ const location = useLocation();
+
+ const state = React.useMemo(
+ () => ({
+ submission_status,
+ setSubmissionStatus,
+ submission_service,
+ setSubmissionService,
+ selected_country,
+ setSelectedCountry,
+ }),
+ [selected_country, submission_service, submission_status]
+ );
+
+ React.useEffect(() => {
+ setSubmissionStatus(submission_status_code.selecting);
+ setSubmissionService(service_code.idv);
+ setSelectedCountry({});
+ }, [location.pathname]);
+
+ return {children} ;
+};
diff --git a/src/components/shared/helpers/index.ts b/src/components/shared/helpers/index.ts
new file mode 100644
index 00000000..009efb2c
--- /dev/null
+++ b/src/components/shared/helpers/index.ts
@@ -0,0 +1 @@
+export * from './context/poi-context';
diff --git a/src/components/shared/index.ts b/src/components/shared/index.ts
new file mode 100644
index 00000000..693dea0c
--- /dev/null
+++ b/src/components/shared/index.ts
@@ -0,0 +1,39 @@
+export * from './services';
+export * from './utils/array';
+export * from './utils/brand';
+export * from './utils/browser';
+export * from './utils/cfd';
+export * from './utils/config';
+export * from './utils/constants';
+export * from './utils/contract';
+export * from './utils/currency';
+export * from './utils/dom';
+export * from './utils/date';
+export * from './utils/digital-options';
+export * from './utils/files';
+export * from './utils/helpers';
+export * from './utils/hooks';
+export * from './utils/loader';
+export * from './utils/loader-handler';
+export * from './utils/location';
+export * from './utils/login';
+export * from './utils/number';
+export * from './utils/object';
+export * from './utils/os';
+export * from './utils/promise';
+export * from './utils/platform';
+export * from './utils/route';
+export * from './utils/routes';
+export * from './utils/screen';
+export * from './utils/shortcode';
+export * from './utils/storage';
+export * from './utils/string';
+export * from './utils/url';
+export * from './utils/validation';
+export * from './utils/validator';
+export * from './services';
+export * from './utils/helpers';
+export * from './utils/constants';
+export * from './utils/loader-handler';
+export * from './utils/types';
+export * from './helpers';
diff --git a/src/components/shared/loaders/deriv-account-loader.js b/src/components/shared/loaders/deriv-account-loader.js
new file mode 100644
index 00000000..c48f9956
--- /dev/null
+++ b/src/components/shared/loaders/deriv-account-loader.js
@@ -0,0 +1,47 @@
+const resolve = require('path').resolve;
+const existsSync = require('fs').existsSync;
+/* Using this loader you can import components from @deriv/components without having to manually
+import the corresponding stylesheet. The deriv-account-loader will automatically import
+stylesheets.
+
+ import { PoaExpired } from '@deriv/account';
+ ↓ ↓ ↓
+ import PoaExpired from '@deriv/account/dist/js/poa-expired';
+*/
+
+function getKebabCase(str) {
+ return str
+ .split(/(?=[A-Z])/)
+ .join('-')
+ .toLowerCase();
+}
+
+function checkExists(component) {
+ return existsSync(resolve(__dirname, '../../../account/src/Components/', component, `${component}.scss`));
+}
+
+module.exports = function (source, map) {
+ const lines = source.split(/\n/);
+ const mapped_lines = lines.map(line => {
+ const matches = /\s*import\s+\{(.*)\}\s*from\s+\'@deriv\/account/.exec(line); // eslint-disable-line no-useless-escape
+ if (!matches || !matches[1]) {
+ return line; // do nothing;
+ }
+ const components = matches[1]
+ .replace(/\sas\s\w+/, '') // Remove aliasing from imports.
+ .replace(/\s+/g, '')
+ .split(',');
+ const replace = components
+ .map(
+ c => `
+import ${c} from '@deriv/account/dist/account/js/${getKebabCase(c)}';
+${checkExists(getKebabCase(c)) ? `import '@deriv/account/dist/account/css/${getKebabCase(c)}.css';` : ''}
+ `
+ )
+ .join('\n');
+
+ return replace;
+ });
+
+ return this.callback(null, mapped_lines.join('\n'), map);
+};
diff --git a/src/components/shared/loaders/deriv-cashier-loader.js b/src/components/shared/loaders/deriv-cashier-loader.js
new file mode 100644
index 00000000..42c0fb89
--- /dev/null
+++ b/src/components/shared/loaders/deriv-cashier-loader.js
@@ -0,0 +1,47 @@
+const resolve = require('path').resolve;
+const existsSync = require('fs').existsSync;
+/* Using this loader you can import components from @deriv/components without having to manually
+import the corresponding stylesheet. The deriv-cashier-loader will automatically import
+stylesheets.
+
+ import { PoaExpired } from '@deriv/cashier';
+ ↓ ↓ ↓
+ import PoaExpired from '@deriv/cashier/dist/js/poa-expired';
+*/
+
+function getKebabCase(str) {
+ return str
+ .split(/(?=[A-Z])/)
+ .join('-')
+ .toLowerCase();
+}
+
+function checkExists(component) {
+ return existsSync(resolve(__dirname, '../../../cashier/src/Components/', component, `${component}.scss`));
+}
+
+module.exports = function (source, map) {
+ const lines = source.split(/\n/);
+ const mapped_lines = lines.map(line => {
+ const matches = /\s*import\s+\{(.*)\}\s*from\s+\'@deriv\/cashier/.exec(line); // eslint-disable-line no-useless-escape
+ if (!matches || !matches[1]) {
+ return line; // do nothing;
+ }
+ const components = matches[1]
+ .replace(/\sas\s\w+/, '') // Remove aliasing from imports.
+ .replace(/\s+/g, '')
+ .split(',');
+ const replace = components
+ .map(
+ c => `
+import ${c} from '@deriv/cashier/dist/cashier/js/${getKebabCase(c)}';
+${checkExists(getKebabCase(c)) ? `import '@deriv/cashier/dist/cashier/css/${getKebabCase(c)}.css';` : ''}
+ `
+ )
+ .join('\n');
+
+ return replace;
+ });
+
+ return this.callback(null, mapped_lines.join('\n'), map);
+};
diff --git a/src/components/shared/loaders/deriv-cfd-loader.js b/src/components/shared/loaders/deriv-cfd-loader.js
new file mode 100644
index 00000000..ad916185
--- /dev/null
+++ b/src/components/shared/loaders/deriv-cfd-loader.js
@@ -0,0 +1,39 @@
+const resolve = require('path').resolve;
+const existsSync = require('fs').existsSync;
+/* Using this loader you can import components from @deriv/cfd without having to manually
+import the corresponding stylesheet. The deriv-account-loader will automatically import
+stylesheets.
+ import { CFDStore } from '@deriv/cfd';
+ ↓ ↓ ↓
+ import CFDStore from '@deriv/cfd/dist/js/CFDStore';
+*/
+
+function checkExists(component) {
+ return existsSync(resolve(__dirname, '../../../cfd/src/Components/', component, `${component}.scss`));
+}
+
+module.exports = function (source, map) {
+ const lines = source.split(/\n/);
+ const mapped_lines = lines.map(line => {
+ const matches = /\s*import\s+\{(.*)\}\s*from\s+\'@deriv\/cfd/.exec(line); // eslint-disable-line no-useless-escape
+ if (!matches || !matches[1]) {
+ return line; // do nothing;
+ }
+ const components = matches[1]
+ .replace(/\sas\s\w+/, '') // Remove aliasing from imports.
+ .replace(/\s+/g, '')
+ .split(',');
+ const replace = components
+ .map(
+ c => `
+import ${c} from '@deriv/cfd/dist/cfd/js/${c}';
+${checkExists(c) ? `import '@deriv/cfd/dist/cfd/css/${c}.css';` : ''}
+ `
+ )
+ .join('\n');
+
+ return replace;
+ });
+
+ return this.callback(null, mapped_lines.join('\n'), map);
+};
diff --git a/src/components/shared/loaders/deriv-reports-loader.js b/src/components/shared/loaders/deriv-reports-loader.js
new file mode 100644
index 00000000..ba5aaa36
--- /dev/null
+++ b/src/components/shared/loaders/deriv-reports-loader.js
@@ -0,0 +1,47 @@
+const resolve = require('path').resolve;
+const existsSync = require('fs').existsSync;
+/* Using this loader you can import components from @deriv/components without having to manually
+import the corresponding stylesheet. The deriv-reports-loader will automatically import
+stylesheets.
+
+ import { PoaExpired } from '@deriv/reports';
+ ↓ ↓ ↓
+ import PoaExpired from '@deriv/reports/dist/js/poa-expired';
+*/
+
+function getKebabCase(str) {
+ return str
+ .split(/(?=[A-Z])/)
+ .join('-')
+ .toLowerCase();
+}
+
+function checkExists(component) {
+ return existsSync(resolve(__dirname, '../../../reports/src/Components/', component, `${component}.scss`));
+}
+
+module.exports = function (source, map) {
+ const lines = source.split(/\n/);
+ const mapped_lines = lines.map(line => {
+ const matches = /\s*import\s+\{(.*)\}\s*from\s+\'@deriv\/reports/.exec(line); // eslint-disable-line no-useless-escape
+ if (!matches || !matches[1]) {
+ return line; // do nothing;
+ }
+ const components = matches[1]
+ .replace(/\sas\s\w+/, '') // Remove aliasing from imports.
+ .replace(/\s+/g, '')
+ .split(',');
+ const replace = components
+ .map(
+ c => `
+import ${c} from '@deriv/reports/dist/reports/js/${getKebabCase(c)}';
+${checkExists(getKebabCase(c)) ? `import '@deriv/reports/dist/reports/css/${getKebabCase(c)}.css';` : ''}
+ `
+ )
+ .join('\n');
+
+ return replace;
+ });
+
+ return this.callback(null, mapped_lines.join('\n'), map);
+};
diff --git a/src/components/shared/loaders/deriv-trader-loader.js b/src/components/shared/loaders/deriv-trader-loader.js
new file mode 100644
index 00000000..a4063926
--- /dev/null
+++ b/src/components/shared/loaders/deriv-trader-loader.js
@@ -0,0 +1,40 @@
+const resolve = require('path').resolve;
+const existsSync = require('fs').existsSync;
+/* Using this loader you can import components from @deriv/trader without having to manually
+import the corresponding stylesheet. The deriv-account-loader will automatically import
+stylesheets.
+
+ import { CFDStore } from '@deriv/trader';
+ ↓ ↓ ↓
+ import CFDStore from '@deriv/trader/dist/js/CFDStore';
+*/
+
+function checkExists(component) {
+ return existsSync(resolve(__dirname, '../../../trader/src/Components/', component, `${component}.scss`));
+}
+
+module.exports = function (source, map) {
+ const lines = source.split(/\n/);
+ const mapped_lines = lines.map(line => {
+ const matches = /\s*import\s+\{(.*)\}\s*from\s+\'@deriv\/trader/.exec(line); // eslint-disable-line no-useless-escape
+ if (!matches || !matches[1]) {
+ return line; // do nothing;
+ }
+ const components = matches[1]
+ .replace(/\sas\s\w+/, '') // Remove aliasing from imports.
+ .replace(/\s+/g, '')
+ .split(',');
+ const replace = components
+ .map(
+ c => `
+import ${c} from '@deriv/trader/dist/trader/js/${c}';
+${checkExists(c) ? `import '@deriv/trader/dist/trader/css/${c}.css';` : ''}
+ `
+ )
+ .join('\n');
+
+ return replace;
+ });
+
+ return this.callback(null, mapped_lines.join('\n'), map);
+};
diff --git a/src/components/shared/loaders/react-import-loader.js b/src/components/shared/loaders/react-import-loader.js
new file mode 100644
index 00000000..ee6ddef2
--- /dev/null
+++ b/src/components/shared/loaders/react-import-loader.js
@@ -0,0 +1,15 @@
+module.exports = function (source, map) {
+ if (/import\s*React,/.test(source)) {
+ this.emitError(
+ new Error(
+ 'Please do not use named imports for React. Use the format `import React` with the `React.` prefix.'
+ )
+ );
+ }
+
+ return this.callback(
+ null,
+ source.replace(/import(\s*)React(\s*)from 'react';/, "import$1* as React$2from 'react';"),
+ map
+ );
+};
diff --git a/src/components/shared/services/index.ts b/src/components/shared/services/index.ts
new file mode 100644
index 00000000..233ebcf6
--- /dev/null
+++ b/src/components/shared/services/index.ts
@@ -0,0 +1,2 @@
+export * from './ws-methods';
+export * from './performance-metrics-methods';
diff --git a/src/components/shared/services/performance-metrics-methods.ts b/src/components/shared/services/performance-metrics-methods.ts
new file mode 100644
index 00000000..0955615a
--- /dev/null
+++ b/src/components/shared/services/performance-metrics-methods.ts
@@ -0,0 +1,61 @@
+import { Analytics } from '@deriv-com/analytics';
+
+import { isMobile } from '../utils/screen';
+
+declare global {
+ interface Window {
+ performance_metrics: {
+ create_ctrader_account_time: number;
+ create_dxtrade_account_time: number;
+ create_mt5_account_time: number;
+ load_cashier_time: number;
+ load_crypto_deposit_cashier_time: number;
+ load_fiat_deposit_cashier_time: number;
+ login_time: number;
+ redirect_from_deriv_com_time: number;
+ signup_time: number;
+ switch_currency_accounts_time: number;
+ switch_from_demo_to_real_time: number;
+ switch_from_real_to_demo_time: number;
+ options_multipliers_section_loading_time: number;
+ };
+ }
+}
+
+// action type will be updated based on the type from Analytics package when it will be updated
+export const startPerformanceEventTimer = (action: keyof typeof global.Window.prototype.performance_metrics) => {
+ if (!window.performance_metrics) {
+ window.performance_metrics = {
+ create_ctrader_account_time: 0,
+ create_dxtrade_account_time: 0,
+ create_mt5_account_time: 0,
+ load_cashier_time: 0,
+ load_crypto_deposit_cashier_time: 0,
+ load_fiat_deposit_cashier_time: 0,
+ login_time: 0,
+ redirect_from_deriv_com_time: 0,
+ signup_time: 0,
+ switch_currency_accounts_time: 0,
+ switch_from_demo_to_real_time: 0,
+ switch_from_real_to_demo_time: 0,
+ options_multipliers_section_loading_time: 0,
+ };
+ }
+
+ window.performance_metrics[action] = Date.now();
+};
+
+export const setPerformanceValue = (action: keyof typeof global.Window.prototype.performance_metrics) => {
+ if (window.performance_metrics?.[action]) {
+ const value = (Date.now() - window.performance_metrics[action]) / 1000;
+ window.performance_metrics[action] = 0;
+
+ const event_name = 'ce_traders_hub_performance_metrics';
+ // @ts-expect-error types will be added in the next version of analytics package
+ Analytics.trackEvent(event_name, {
+ action,
+ value,
+ device: isMobile() ? 'mobile' : 'desktop',
+ });
+ }
+};
diff --git a/src/components/shared/services/ws-methods.ts b/src/components/shared/services/ws-methods.ts
new file mode 100644
index 00000000..aa845dfb
--- /dev/null
+++ b/src/components/shared/services/ws-methods.ts
@@ -0,0 +1,13 @@
+// eslint-disable-next-line import/no-mutable-exports
+export let WS: Record;
+
+export const setWebsocket = (websocket: object) => {
+ WS = websocket;
+};
+
+/**
+ * A temporarily custom hook to expose the global `WS` object from the `shared` package.
+ */
+export const useWS = () => {
+ return WS;
+};
diff --git a/src/components/shared/styles/constants.scss b/src/components/shared/styles/constants.scss
new file mode 100644
index 00000000..c7650bef
--- /dev/null
+++ b/src/components/shared/styles/constants.scss
@@ -0,0 +1,249 @@
+/*------------------------------------*
+ * # Constants
+ *------------------------------------*/
+
+/*RTL Language Mixin*/
+
+@mixin rtl {
+ [dir='rtl'] & {
+ @content;
+ }
+}
+
+/* stylelint-disable color-no-hex */
+
+/* COLOR PALETTE */
+
+/* colors */
+$color-black: #0e0e0e;
+$color-black-1: #333333;
+$color-black-3: #151717;
+$color-black-4: #1d1f20;
+$color-black-5: #242828;
+$color-black-6: #3e3e3e;
+$color-black-7: #000000;
+$color-black-8: #323738;
+$color-black-9: #5c5c5c;
+$color-blue: #377cfc;
+$color-blue-1: #0dc2e7;
+$color-blue-2: #2a3052;
+$color-blue-3: #0796e0;
+$color-blue-4: #0677af;
+$color-blue-5: #dfeaff;
+$color-blue-6: #92b8ff;
+$color-blue-7: #182130;
+$color-blue-8: #e6f5ff;
+$color-brown: #664407;
+$color-green: #85acb0;
+$color-green-1: #4bb4b3;
+$color-green-2: #3d9494;
+$color-green-3: #00a79e;
+$color-green-4: #008079;
+$color-green-5: #4bb4b329;
+$color-green-6: #17eabd;
+$color-green-7: #e8fdf8;
+$color-green-8: #cedddf;
+$color-grey: #c2c2c2;
+$color-grey-1: #999999;
+$color-grey-2: #f2f3f4;
+$color-grey-3: #eaeced;
+$color-grey-4: #e6e9e9;
+$color-grey-5: #d6dadb;
+$color-grey-6: #d6d6d6;
+$color-grey-7: #6e6e6e;
+$color-grey-8: #d7d7d7;
+$color_grey-9: #868686;
+$color-grey-10: #919191;
+$color-grey-11: #fafafa;
+$color-grey-12: #f5f7fa;
+$color-grey-13: #2e2e2e;
+$color-grey-14: #e2e5e7;
+$color-orange: #ff6444;
+$color-purple: #722fe4;
+$color-red: #ff444f;
+$color-red-1: #ec3f3f;
+$color-red-2: #cc2e3d;
+$color-red-3: #a32430;
+$color-red-4: #d33636;
+$color-red-5: #eb3e48;
+$color-red-6: #ec3f3f29;
+$color-red-7: #ffe1e3;
+$color-red-8: #661b20;
+$color-red-9: #b33037;
+$color-red-10: #ff444f;
+$color-red-11: #fce3e3;
+$color-violet: #4a3871;
+$color-white: #ffffff;
+$color-yellow: #ffad3a;
+$color-yellow-1: #b3760d;
+$color-yellow-2: #ffa912;
+$color-yellow-3: rgba(255, 173, 58, 0.16);
+
+/* status colors */
+$color-status-warning: rgba(255, 173, 58, 0.16);
+$color-status-information: rgba(55, 124, 252, 0.16);
+$color-status-announcement: rgba(75, 180, 179, 0.16);
+$color-status-error: rgba(236, 63, 63, 0.16);
+
+/* alpha colors */
+$alpha-color-black-1: transparentize($color-black-7, 0.28);
+$alpha-color-black-2: transparentize($color-black, 0.04);
+$alpha-color-black-3: transparentize($color-black-7, 0.92);
+$alpha-color-black-4: transparentize($color-black-7, 0.84);
+$alpha-color-black-5: transparentize($color-black-7, 0.16);
+$alpha-color-black-6: transparentize($color-black-7, 0.36);
+$alpha-color-black-7: transparentize($color-black, 0.5);
+$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);
+$alpha-color-white-4: transparentize($color-white, 0.3);
+$alpha-color-red-1: transparentize($color-red, 0.92);
+$alpha-color-red-2: transparentize($color-red, 0.84);
+$alpha-color-red-3: transparentize($color-red, 0.76);
+$alpha-color-green-1: transparentize($color-green-1, 0.08);
+$alpha-color-green-2: transparentize($color-green-3, 0.08);
+$alpha-color-yellow-1: transparentize($color-yellow, 0.84);
+
+/* gradient colors */
+$gradient-color-green-1: linear-gradient(to top, $color-white, transparentize($color-green-1, 0.84));
+$gradient-color-green-2: linear-gradient(to top, $color-black, transparentize($color-green-3, 0.84));
+$gradient-color-red-1: linear-gradient(to top, $color-white, transparentize($color-red, 0.84));
+$gradient-color-red-2: linear-gradient(to top, $color-black, transparentize($color-red, 0.84));
+$contract-gradient-color-red-1: linear-gradient(to top, $color-white 85%, transparentize($color-red, 0.84));
+$contract-gradient-color-red-2: linear-gradient(to top, $color-black 85%, transparentize($color-red, 0.84));
+$gradient-color-white: linear-gradient(to right, transparentize($color-white, 1) -5%, $color-white 71%);
+$gradient-color-black: linear-gradient(to right, transparentize($color-black, 1) -5%, $color-black 71%);
+
+$gradient-color-purple: linear-gradient(274.73deg, #5a205d 3.82%, #7f3883 88.49%);
+$gradient-color-blue: linear-gradient(274.73deg, #1a205e 3.82%, #122d96 88.49%);
+$gradient-color-orange: linear-gradient(90deg, #eb001b 0%, #f79e1b 100%);
+$gradient-color-black-2: linear-gradient(274.73deg, #464750 3.82%, #0e0f11 88.49%);
+$gradient-color-orange-2: linear-gradient(90deg, #f89e32 0%, #f7931b 103.12%);
+$gradient-color-blue-2: linear-gradient(90deg, #3d83cf 0%, #2775ca 100%);
+$gradient-color-green-3: linear-gradient(90deg, #98cc53 0%, #8dc640 100%);
+$gradient-color-blue-3: linear-gradient(90deg, #1a77ac 0%, #0068a3 100%);
+$gradient-color-orange-3: linear-gradient(90deg, #ff7635 0%, #ff671f 100%);
+$gradient-color-orange-4: linear-gradient(90deg, #f36938 0%, #f25822 100%);
+$gradient-color-blue-4: linear-gradient(90deg, #1a8fff 0%, #0083ff 100%);
+$gradient-color-red-3: linear-gradient(90deg, #ff444f 0%, #211d1e 95.22%);
+$gradient-color-red-4: linear-gradient(90deg, #ff6444 0%, #ff444f 100%);
+$gradient-color-black-3: linear-gradient(58.51deg, #061828 28.06%, #1a3c60 93.51%);
+$gradient-color-black-4: linear-gradient(274.25deg, #333333 9.01%, #5c5b5b 103.31%);
+$gradient-color-black-5: linear-gradient(180deg, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.16) 100%);
+$gradient-color-white-2: linear-gradient(180deg, rgba(255, 255, 255, 0) 50%, rgba(255, 255, 255, 0.16) 100%);
+$gradient-color-blue-5: linear-gradient(90deg, #00a8af 0%, #04cfd8 104.41%);
+$gradient-color-gold: linear-gradient(90deg, #f7931a 0%, #ffc71b 104.41%);
+$gradient-color-green-4: linear-gradient(90deg, #1db193 0%, #09da7a 104.41%);
+
+/* Preserve legacy variables */
+/* Primary */
+
+$COLOR_BLACK: #000000;
+$COLOR_BLACK_2: #1d1f20;
+$COLOR_BLACK_3: #0e0e0e;
+$COLOR_GREEN_1: #39b19d;
+$COLOR_GREEN_2: #2d9f93;
+$COLOR_GREEN_3: #21ce99;
+$COLOR_GREEN_4: #00a79e;
+$COLOR_GREEN_5: #4bb4b3;
+$COLOR_ORANGE: #ff9933;
+$COLOR_DARK_ORANGE: #ff8802;
+$COLOR_PURPLE: #4f60ae;
+$COLOR_RED: #e31c4b;
+$COLOR_RED_2: #cc2e3d;
+$COLOR_RED_3: #ec3f3f;
+$COLOR_CORAL_RED: #ff444f;
+$COLOR_SKY_BLUE: #2196f3;
+$COLOR_WHITE: #ffffff;
+$COLOR_BLUE: #1c5ae3;
+// Light theme
+$COLOR_LIGHT_BLACK_1: rgba(0, 0, 0, 0.8);
+$COLOR_LIGHT_BLACK_2: rgba(0, 0, 0, 0.4);
+$COLOR_LIGHT_BLACK_3: rgba(0, 0, 0, 0.16);
+$COLOR_LIGHT_BLACK_3_SOLID_1: #d6d6d6;
+$COLOR_LIGHT_BLACK_3_SOLID_2: #b3b3b3;
+$COLOR_LIGHT_BLACK_4: rgba(0, 0, 0, 0.04);
+$COLOR_LIGHT_BLACK_4_SOLID: #f4f4f6;
+$COLOR_LIGHT_GRAY_1: #999cac;
+$COLOR_LIGHT_GRAY_2: rgba(153, 156, 172, 0.32);
+$COLOR_LIGHT_GRAY_3: #eaeced;
+$COLOR_LIGHT_GRAY_4: #6e6e6e;
+$COLOR_LIGHT_GRAY_5: #c2c2c2;
+$COLOR_LIGHT_GRAY_6: #f2f3f4;
+$COLOR_LIGHT_GREEN_GRADIENT: linear-gradient(to top, rgba(255, 255, 255, 0), rgba(0, 148, 117, 0.16));
+$COLOR_LIGHT_RED_GRADIENT: linear-gradient(to top, rgba(255, 255, 255, 0), rgba(227, 28, 75, 0.16));
+$COLOR_LIGHT_WHITE_GRADIENT: linear-gradient(to right, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+// Dark theme
+$COLOR_DARK_BLACK_GRADIENT: linear-gradient(to right, rgba(16, 19, 32, 1), rgba(16, 19, 32, 0));
+$COLOR_DARK_BLUE_1: #0b0e18;
+$COLOR_DARK_BLUE_2: #101320;
+$COLOR_DARK_BLUE_3: #191c31;
+$COLOR_DARK_BLUE_4: #202641;
+$COLOR_DARK_BLUE_5: #2a3052;
+$COLOR_DARK_BLUE_6: #555975;
+$COLOR_DARK_BLUE_7: #7f8397;
+$COLOR_DARK_BLUE_8: rgba(127, 131, 151, 0.3);
+$COLOR_DARK_GREEN_GRADIENT: linear-gradient(to top, rgba(16, 19, 32, 0), rgba(0, 148, 117, 0.16));
+$COLOR_DARK_RED_GRADIENT: linear-gradient(to top, rgba(16, 19, 32, 0), rgba(227, 28, 75, 0.16));
+$COLOR_DARK_GRAY_1: #282a37;
+$COLOR_DARK_GRAY_2: #303342;
+$COLOR_DARK_GRAY_3: #555975;
+$COLOR_DARK_GRAY_4: #999999;
+/* stylelint-enable color-no-hex */
+
+$BORDER_RADIUS: 4px;
+$BORDER_RADIUS_2: 8px;
+
+$MAX_CONTAINER_WIDTH: 1440px;
+
+$POSITIONS_DRAWER_WIDTH: 240px;
+$POSITIONS_DRAWER_MARGIN: 8px;
+
+$HEADER_HEIGHT: 48px;
+$FOOTER_HEIGHT: 36px;
+
+$MOBILE_HEADER_HEIGHT: 40px;
+$MOBILE_HEADER_HEIGHT_DASHBOARD: 48px;
+$MOBILE_WRAPPER_HEADER_HEIGHT: 40px;
+$MOBILE_WRAPPER_FOOTER_HEIGHT: 70px;
+
+$SIDEBAR_WIDTH: 240px;
+
+/* Wallet gradient background */
+$gradient-airtm: linear-gradient(90deg, #1a8fff 0%, #0083ff 100%);
+$gradient-banxa: linear-gradient(90deg, #000000 0%, #4ac0ba 96.35%);
+$gradient-bitcoin: linear-gradient(90deg, #f89e32 0%, #f7931b 103.12%);
+$gradient-credit: linear-gradient(274.73deg, #464750 3.82%, #0e0f11 88.49%);
+$gradient-dp2p: linear-gradient(90deg, #ff444f 0%, #211d1e 95.22%);
+$gradient-fasapay: linear-gradient(90deg, #f6931c 0%, #4873ac 95.22%);
+$gradient-jeton: linear-gradient(90deg, #ff7635 0%, #ff671f 100%);
+$gradient-mastercard: linear-gradient(90deg, #eb001b 0%, #f79e1b 100%);
+$gradient-neteller: linear-gradient(90deg, #98cc53 0%, #8dc640 100%);
+$gradient-paylivre: linear-gradient(90deg, #352caa 0%, #9a6bfc 100%);
+$gradient-paysafe: linear-gradient(90deg, #e3001b 0%, #008ac9 100%);
+$gradient-perfectmoney: linear-gradient(90deg, #f12c2c 0%, #ef1515 100%);
+$gradient-skrill: linear-gradient(274.73deg, #5a205d 3.82%, #7f3883 88.49%);
+$gradient-sticpay: linear-gradient(90deg, #f36938 0%, #f25822 100%);
+$gradient-virtual: linear-gradient(90deg, #ff6444 0%, #ff444f 100%);
+$gradient-visa: linear-gradient(274.73deg, #1a205e 3.82%, #122d96 88.49%);
+$gradient-webmoney: linear-gradient(90deg, #1a77ac 0%, #0068a3 100%);
+
+/* App Cards gradient background */
+$gradient-virtual: linear-gradient(274.25deg, #333333 9.01%, #5c5b5b 103.31%);
+$gradient-virtual-swap-free: linear-gradient(58.51deg, #061828 28.06%, #1a3c60 93.51%);
+
+/* Wallets */
+$ready-banner-bg-color: #e2f3f3;
+$ready-banner-tick-bg-color: #4ab4b3;
+$wallet-demo-bg-color: #fff8f9;
+$wallet-dark-demo-bg-color: #140506;
+$wallet-demo-divider-color: #fff0f1;
+$wallet-box-shadow:
+ 0px 12px 16px -4px rgba(14, 14, 14, 0.08),
+ 0px 4px 6px -2px rgba(14, 14, 14, 0.03);
+$btn-shadow:
+ 0px 24px 24px 0px rgba(0, 0, 0, 0.08),
+ 0px 0px 24px 0px rgba(0, 0, 0, 0.08);
diff --git a/src/components/shared/styles/devices.scss b/src/components/shared/styles/devices.scss
new file mode 100644
index 00000000..5b2749a4
--- /dev/null
+++ b/src/components/shared/styles/devices.scss
@@ -0,0 +1,62 @@
+/**
+ * Define Breakpoints here.
+ */
+$mobile-width: 320px;
+$tablet-width: 768px;
+$desktop-width: 1024px;
+
+@mixin tablet {
+ @media (min-width: #{$tablet-width}) and (max-width: #{$desktop-width - 1px}) {
+ @content;
+ }
+}
+
+@mixin desktop {
+ @media (min-width: #{$desktop-width}) {
+ @content;
+ }
+}
+
+@mixin tablet-down {
+ @media (max-width: #{$tablet-width}) {
+ @content;
+ }
+}
+
+@mixin tablet-up {
+ @media (min-width: #{$tablet-width}) {
+ @content;
+ }
+}
+
+@mixin mobile-up {
+ @media (min-width: #{$mobile-width}) {
+ @content;
+ }
+}
+
+@mixin mobile {
+ @media (min-width: #{$mobile-width}) and (max-width: #{$tablet-width - 1}) {
+ @content;
+ }
+}
+
+@mixin mobile-landscape {
+ @media only screen and (min-width: #{$mobile-width}) and (max-width: #{$desktop-width - 1}) and (orientation: landscape) {
+ @content;
+ }
+}
+
+@mixin touch-device {
+ // add css interaction media query to detect touch devices
+ // refer to: https://caniuse.com/#feat=css-media-interaction
+ @media (pointer: coarse) {
+ @content;
+ }
+}
+
+@mixin mobile-tablet-mix {
+ @media (min-width: #{$mobile-width}) and (max-width: #{$desktop-width - 1px}) {
+ @content;
+ }
+}
diff --git a/src/components/shared/styles/fonts.scss b/src/components/shared/styles/fonts.scss
new file mode 100644
index 00000000..d0f599dd
--- /dev/null
+++ b/src/components/shared/styles/fonts.scss
@@ -0,0 +1,73 @@
+/*
+ Constants
+*/
+$FONT_STACK: 'IBM Plex Sans', sans-serif;
+$BASE_FONT_SIZE: 10px;
+
+/*
+ Variables
+*/
+$FONT_SIZES: (
+ 'xheading' 3.2rem,
+ 'heading' 3rem,
+ 'large' 2.8rem,
+ 'title' 1.6rem,
+ 'paragraph' 1.4rem,
+ 'small' 1.2rem,
+ 'xsmall' 1rem,
+ 'xxsmall' 0.8rem,
+ 'xxxsmall' 0.6rem
+);
+
+$FONT_COLORS: (
+ 'active' var(--text-colored-background),
+ 'prominent' var(--text-prominent),
+ 'black' var(--text-general),
+ 'grey' var(--text-less-prominent),
+ 'disabled' var(--text-disabled),
+ 'green' var(--text-profit-success),
+ 'red' var(--text-loss-danger)
+);
+
+$FONT_WEIGHTS: ('bold' 700, 'semibold' 500, 'normal' 400, 'light' 300);
+
+$TEXT_ALIGN: ('center' center, 'left' start, 'right' end);
+
+$LINEHEIGHTS: (
+ 'large': 1.75,
+ 'medium': 1.5,
+ 'small': 1.25,
+ 'xsmall': 1.4,
+);
+
+/*
+ * Generate typefaces key-value pair of variable name and config
+ * Example:
+ --paragraph-center-bold-black: (14px, center, bold, black)
+ */
+@function generate-typefaces() {
+ $typeface_list: ();
+ @each $fontsize_name, $size in $FONT_SIZES {
+ @each $textalign_name, $text_align in $TEXT_ALIGN {
+ @each $color_name, $color in $FONT_COLORS {
+ @each $fontweight_name, $weight in $FONT_WEIGHTS {
+ $key: --#{$fontsize_name}-#{$textalign_name}-#{$fontweight_name}-#{$color_name};
+ $val: ($size, $text_align, $weight, $color);
+ $typeface: (
+ $key: $val,
+ );
+ $typeface_list: map-merge($typeface_list, $typeface);
+ }
+ }
+ }
+ }
+ // @debug $typeface_list; /* uncomment to debug */
+ @return $typeface_list;
+}
+
+/*
+ List of all typefaces variables in the format: --$FONT_SIZES-$TEXT_ALIGN-$FONT_WEIGHTS-COLORS.
+ See $FONT_SIZES, $TEXT_ALIGN, $FONT_WEIGHTS, $COLORS maps above for references.
+ Variables name example: --title-center-semibold-red
+ */
+$TYPEFACES_LIST: generate-typefaces();
diff --git a/src/components/shared/styles/google-fonts.scss b/src/components/shared/styles/google-fonts.scss
new file mode 100644
index 00000000..2a301512
--- /dev/null
+++ b/src/components/shared/styles/google-fonts.scss
@@ -0,0 +1,3 @@
+@at-root {
+ @import url('https://fonts.googleapis.com/css?family=IBM+Plex+Sans:300,400,500,700&display=swap&subset=cyrillic,cyrillic-ext,latin-ext,vietnamese');
+}
diff --git a/src/components/shared/styles/index.js b/src/components/shared/styles/index.js
new file mode 100644
index 00000000..cb4cc28b
--- /dev/null
+++ b/src/components/shared/styles/index.js
@@ -0,0 +1,5 @@
+const path = require('path');
+
+const resources = ['constants.scss', 'mixins.scss', 'fonts.scss', 'inline-icons.scss', 'devices.scss'];
+
+module.exports = resources.map(file => path.resolve(__dirname, file));
diff --git a/src/components/shared/styles/inline-icons.scss b/src/components/shared/styles/inline-icons.scss
new file mode 100644
index 00000000..cde7c339
--- /dev/null
+++ b/src/components/shared/styles/inline-icons.scss
@@ -0,0 +1,34 @@
+/*
+ to use styles without modifier class on .inline-icon
+ use @extend
+
+ e.g. style icon on parent hover:
+ a:hover .inline-icon {
+ @extend %inline-icon-active;
+ }
+*/
+
+%inline-icon {
+ @include colorIcon(var(--text-general), none);
+
+ &.active,
+ &-active {
+ @include colorIcon(var(--text-prominent), none);
+ }
+ &.disabled,
+ &-disabled {
+ @include colorIcon(var(--text-disabled), none);
+ }
+ &.white,
+ &-white {
+ @include colorIcon(var(--text-prominent));
+ }
+ &.border_hover_color,
+ &-border_hover_color {
+ @include colorIcon(var(--text-prominent));
+ }
+ &.secondary,
+ &-secondary {
+ @include colorIcon(var(--text-less-prominent));
+ }
+}
diff --git a/src/components/shared/styles/mixins.scss b/src/components/shared/styles/mixins.scss
new file mode 100644
index 00000000..697380e4
--- /dev/null
+++ b/src/components/shared/styles/mixins.scss
@@ -0,0 +1,263 @@
+@import './themes.scss';
+@import './devices.scss';
+@import './fonts.scss';
+
+/*------------------------------------*
+ * # SASS Mixins and Functions
+ *------------------------------------*/
+
+/*
+ * SASS interpolation
+ */
+// Requires the calc-interpolation function which can also be used independently
+@function calc-interpolation($min-screen, $min-value, $max-screen, $max-value) {
+ $a: ($max-value - $min-value) / ($max-screen - $min-screen);
+ $b: $min-value - $a * $min-screen;
+
+ $sign: '+';
+ @if ($b < 0) {
+ $sign: '-';
+ $b: abs($b);
+ }
+ @return calc(#{$a * 100}vw #{$sign} #{$b});
+}
+
+@mixin interpolate($properties, $min-screen, $max-screen, $min-value, $max-value) {
+ & {
+ @each $property in $properties {
+ #{$property}: $min-value;
+ }
+ @media screen and (min-width: $min-screen) {
+ @each $property in $properties {
+ #{$property}: calc-interpolation($min-screen, $min-value, $max-screen, $max-value);
+ }
+ }
+ @media screen and (min-width: $max-screen) {
+ @each $property in $properties {
+ #{$property}: $max-value;
+ }
+ }
+ }
+}
+
+/*
+ * PX to EM
+ * @param $px - px value to be converted
+ * @param $base - base font size (in `em`)
+ * Note: 'em' values are calculate based on the element font-size
+ * to properly converts 'px' to 'em', please pass in the element font-size with it
+ * Usage example:
+ padding: em(16px, 1.6em); // font-size in 'em'
+ */
+@function em($px, $base: $BASE_FONT_SIZE) {
+ $list: ();
+ @if length($px) != 1 {
+ @for $i from 1 through length($px) {
+ $val_em: (nth($px, $i) / $base) * 1em;
+ $list: append($list, $val_em, space);
+ }
+ @return $list;
+ } @else {
+ @return ($px / $base) * 1em;
+ }
+}
+
+/*
+ * Set property by passing a property name, and values.
+ * @param $property name - padding, margin etc.
+ * @param $values - values in `px` (space separated for multiple values)
+ * @param $font-size - base font-size in `em`
+ * Usage example:
+ @include toEm(padding, 8px 16px 8px, 1.6em);
+ */
+@mixin toEm($property, $values, $font-size: $BASE_FONT_SIZE) {
+ #{$property}: em($values, $font-size);
+}
+
+/*
+ * Generate typefaces key-value pair of variable name and config
+ * Example:
+ --paragraph-center-bold-black: (14px, center, bold, black)
+ */
+@function generate-typefaces() {
+ $typeface_list: ();
+ @each $fontsize_name, $size in $FONT_SIZES {
+ @each $textalign_name, $text_align in $TEXT_ALIGN {
+ @each $color_name, $color in $FONT_COLORS {
+ @each $fontweight_name, $weight in $FONT_WEIGHTS {
+ $key: --#{$fontsize_name}-#{$textalign_name}-#{$fontweight_name}-#{$color_name};
+ $val: ($size, $text_align, $weight, $color);
+ $typeface: (
+ $key: $val,
+ );
+ $typeface_list: map-merge($typeface_list, $typeface);
+ }
+ }
+ }
+ }
+ // @debug $typeface_list; /* uncomment to debug */
+ @return $typeface_list;
+}
+
+/*
+ * Sets font-size, font-weight, color, text-transform, text-align and line-height
+ * Usage example:
+ @include setTypeface(16px, bold, black, uppercase);
+ */
+@mixin setTypeface($size, $align, $weight, $colour, $tt) {
+ @if $size {
+ font-size: $size;
+ }
+ @if $colour {
+ color: $colour;
+ }
+ @if $weight {
+ font-weight: $weight;
+ }
+ @if $tt {
+ text-transform: $tt;
+ }
+ @if $align {
+ text-align: $align;
+ }
+ @if $size == 1.6rem {
+ line-height: map-get($map: $LINEHEIGHTS, $key: 'large');
+ } @else if $size == 1.4rem {
+ line-height: map-get($map: $LINEHEIGHTS, $key: 'medium');
+ } @else if $size == 1.2rem {
+ line-height: map-get($map: $LINEHEIGHTS, $key: 'small');
+ } @else if $size == 1rem {
+ line-height: map-get($map: $LINEHEIGHTS, $key: 'xsmall');
+ } @else {
+ line-height: unset;
+ }
+}
+
+/*
+ * Set typefaces by passing a typeface variable name
+ * @param $var - typeface variable
+ * @param $tt - text-transform property
+ * @param $align - text-align property
+ * Usage example:
+ @include typeface(--paragraph-bold-black, uppercase, center);
+ */
+@mixin typeface($var, $tt: none) {
+ $typeface_config: map-get(
+ $map: $TYPEFACES_LIST,
+ $key: $var,
+ );
+
+ $size: nth($typeface_config, 1); // font-size
+ $align: nth($typeface_config, 2); // text-align
+ $weight: nth($typeface_config, 3); // font-weight
+ $color: nth($typeface_config, 4); // color
+ @include setTypeface($size, $align, $weight, $color, $tt);
+}
+
+/*
+ * Range Slider Thumb styling is set here
+ */
+@mixin thumbStyle() {
+ appearance: none;
+ width: 1em;
+ height: 1em;
+ border-radius: 100%;
+ border: 0;
+ cursor: pointer;
+ transition: box-shadow 0.2s;
+ box-shadow: 0 0 0 0px rgba(0, 0, 0, 0);
+
+ &:hover {
+ box-shadow: 0 0 0 0.5em var(--state-hover);
+ }
+}
+
+/*
+ * SVG icons colors
+ */
+@mixin colorIcon($colors...) {
+ @for $i from 1 through length($colors) {
+ /* postcss-bem-linter: ignore */
+ .color#{$i}-fill {
+ fill: nth($colors, $i);
+ }
+ /* postcss-bem-linter: ignore */
+ .color#{$i}-stroke {
+ stroke: nth($colors, $i);
+ }
+ }
+}
+
+/*
+ * Bar Spinner Animation
+ * @param $count - number of bars
+ * @param $duration - duration of animation
+ * @param $phase - each bar phase change delay
+ * Usage example:
+ @include createBarspinnerAnimation(5, 1.2s, 0,1);
+ */
+@mixin createBarspinnerAnimation($count, $duration, $phase) {
+ animation: sk-stretchdelay $duration infinite ease-in-out;
+
+ @for $i from 1 through $count {
+ &--#{$i} {
+ animation-delay: -$duration + (($i - 1) * $phase);
+ }
+ }
+}
+
+/*
+ * Tooltip colors
+ * @param $color - color property
+ * Usage example:
+ @include tooltipColor($COLOR_RED);
+ */
+@mixin tooltipColor($color) {
+ &:before {
+ background: $color;
+ }
+ &:after,
+ &[data-tooltip-pos='top']:after {
+ border-top-color: $color;
+ }
+ &[data-tooltip-pos='right']:after {
+ border-top-color: transparent;
+ border-right-color: $color;
+ }
+ &[data-tooltip-pos='left']:after {
+ border-top-color: transparent;
+ border-left-color: $color;
+ }
+ &[data-tooltip-pos='bottom']:after {
+ border-top-color: transparent;
+ border-bottom-color: $color;
+ }
+}
+
+/*
+ * Convert Tooltip colors
+ * @param $color - css color property
+ * Usage example:
+ @include convertTooltipColor(var(--status-default));
+ */
+@mixin convertTooltipColor($color) {
+ &:before {
+ background: $color;
+ }
+ &:after,
+ &[data-tooltip-pos='top']:after {
+ border-top-color: $color;
+ }
+ &[data-tooltip-pos='right']:after {
+ border-top-color: transparent;
+ border-right-color: $color;
+ }
+ &[data-tooltip-pos='left']:after {
+ border-top-color: transparent;
+ border-left-color: $color;
+ }
+ &[data-tooltip-pos='bottom']:after {
+ border-top-color: transparent;
+ border-bottom-color: $color;
+ }
+}
diff --git a/src/components/shared/styles/reset.scss b/src/components/shared/styles/reset.scss
new file mode 100644
index 00000000..86ccca18
--- /dev/null
+++ b/src/components/shared/styles/reset.scss
@@ -0,0 +1,147 @@
+/*------------------------------------*
+ * # Reset SCSS
+ * - to make browsers render all elements more consistently
+ * - Reference: https://github.com/AdamMarsden/simple-typography/blob/master/_reset.scss
+ *------------------------------------*/
+
+/* stylelint-disable */
+html,
+body,
+div,
+span,
+applet,
+object,
+iframe,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+big,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+ins,
+kbd,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+b,
+u,
+i,
+center,
+dl,
+dt,
+dd,
+ol,
+ul,
+li,
+fieldset,
+form,
+label,
+legend,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td,
+article,
+aside,
+canvas,
+details,
+embed,
+figure,
+figcaption,
+footer,
+header,
+hgroup,
+input,
+menu,
+nav,
+output,
+ruby,
+section,
+summary,
+time,
+mark,
+audio,
+video {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font: inherit;
+ vertical-align: baseline;
+}
+/* HTML5 display-role reset for older browsers */
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+menu,
+nav,
+section {
+ display: block;
+}
+
+html {
+ height: 100%;
+ font-size: 10px;
+}
+
+body {
+ line-height: 1;
+ height: 100%;
+}
+
+ol,
+ul {
+ list-style: none;
+}
+
+blockquote,
+q {
+ quotes: none;
+}
+
+blockquote:before,
+blockquote:after,
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+strong {
+ font-weight: bold;
+}
+/* stylelint-enable */
diff --git a/src/components/shared/styles/themes.scss b/src/components/shared/styles/themes.scss
new file mode 100644
index 00000000..f92d1a58
--- /dev/null
+++ b/src/components/shared/styles/themes.scss
@@ -0,0 +1,361 @@
+@import './constants.scss';
+
+:host,
+:root {
+ // Text sizes
+ --text-size-xxxxs: 0.8rem;
+ --text-size-xxxs: 1rem;
+ --text-size-xxs: 1.2rem;
+ --text-size-xs: 1.4rem;
+ --text-size-s: 1.6rem;
+ --text-size-xsm: 1.8rem;
+ --text-size-sm: 2rem;
+ --text-size-m: 2.4rem;
+ --text-size-l: 3.2rem;
+ --text-size-xl: 4.8rem;
+ --text-size-xxl: 6.4rem;
+
+ //Line Height
+ --text-lh-xxs: 1;
+ --text-lh-xs: 1.25;
+ --text-lh-s: 1.4;
+ --text-lh-m: 1.5;
+ --text-lh-l: 1.75;
+ --text-lh-xl: 2;
+ --text-lh-xxl: 2.4;
+
+ //Font Weight
+ --text-weight-lighter: lighter;
+ --text-weight-normal: normal;
+ --text-weight-bold: bold;
+ --text-weight-bolder: bolder;
+
+ //Text Align
+ --text-align-left: start;
+ --text-align-right: right;
+ --text-align-center: center;
+
+ // Brand primary colors
+ --brand-white: #{$color-white};
+ --brand-dark-grey: #{$color-black};
+ --brand-red-coral: #{$color-red};
+ --brand-orange: #{$color-orange};
+ // Brand secondary colors
+ --brand-secondary: #{$color-green};
+
+ // Wallet gradient background
+ --wallet-airtm: #{$gradient-airtm};
+ --wallet-banxa: #{$gradient-banxa};
+ --wallet-bitcoin: #{$gradient-bitcoin};
+ --wallet-credit: #{$gradient-credit};
+ --wallet-dp2p: #{$gradient-dp2p};
+ --wallet-fasapay: #{$gradient-fasapay};
+ --wallet-jeton: #{$gradient-jeton};
+ --wallet-mastercard: #{$gradient-mastercard};
+ --wallet-neteller: #{$gradient-neteller};
+ --wallet-paylivre: #{$gradient-paylivre};
+ --wallet-paysafe: #{$gradient-paysafe};
+ --wallet-perfectmoney: #{gradient-perfectmoney};
+ --wallet-skrill: #{$gradient-skrill};
+ --wallet-sticpay: #{$gradient-sticpay};
+ --wallet-virtual: #{$gradient-virtual};
+ --wallet-visa: #{$gradient-visa};
+ --wallet-webmoney: #{$gradient-webmoney};
+
+ // App Cards gradient background
+ --app-card-virtual: #{$gradient-virtual};
+ --app-card-virtual-swap-free: #{$gradient-virtual-swap-free};
+
+ .theme--light {
+ // General
+ --general-main-1: #{$color-white};
+ --general-main-2: #{$color-white};
+ --general-main-3: #{$color-grey-1};
+ --general-main-4: #{$alpha-color-white-4};
+ --general-section-1: #{$color-grey-2};
+ --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-section-6: #{$color-grey-2};
+ --general-disabled: #{$color-grey-3};
+ --general-hover: #{$color-grey-4};
+ --general-active: #{$color-grey-5};
+ // Icons and Texts
+ --text-general: #{$color-black-1};
+ --text-primary: #{$color-grey-1};
+ --text-less-prominent: #{$color-grey-1};
+ --text-prominent: #{$color-black-1};
+ --text-disabled: #{$color-grey-1};
+ --text-disabled-1: #{$color-grey-6};
+ --text-loss-danger: #{$color-red-1};
+ --text-profit-success: #{$color-green-1};
+ --text-warning: #{$color-yellow};
+ --text-red: #{$color-red};
+ --text-green: #{$color-green-6};
+ --text-blue: #{$color-blue-3};
+ --text-info-blue: #{$color-blue};
+ --text-info-blue-background: #{$color-blue-5};
+ --text-colored-background: #{$color-white};
+ --icon-light-background: #{$color-black-9};
+ --icon-dark-background: #{$color-white};
+ --icon-grey-background: #{$color-grey-2};
+ --icon-black-plus: #{$color-black-7};
+ --text-status-info-blue: #{$color-blue};
+ --text-hint: #{$color-black-1};
+ // Purchase
+ --purchase-main-1: #{$color-green-1};
+ --purchase-section-1: #{$color-green-2};
+ --purchase-main-2: #{$color-red-1};
+ --purchase-section-2: #{$color-red-4};
+ --purchase-disabled-main: #{$color-grey-3};
+ --purchase-disabled-section: #{$color-grey-4};
+ // Buttons
+ --button-primary-default: var(--brand-red-coral);
+ --button-secondary-default: #{$color-grey-1};
+ --button-tertiary-default: transparent;
+ --button-primary-light-default: #{$alpha-color-red-2};
+ --button-primary-hover: #{$color-red-5};
+ --button-secondary-hover: #{$alpha-color-black-3};
+ --button-tertiary-hover: #{$alpha-color-red-1};
+ --button-primary-light-hover: #{$alpha-color-red-3};
+ --button-toggle-primary: #{$color-blue-3};
+ --button-toggle-secondary: #{$color-grey-5};
+ --button-toggle-alternate: #{$color-white};
+ // Overlay
+ --overlay-outside-dialog: #{$alpha-color-black-1};
+ --overlay-inside-dialog: #{$alpha-color-white-1};
+ // Shadow
+ --shadow-menu: #{$alpha-color-black-4};
+ --shadow-menu-2: #{$alpha-color-black-4};
+ --shadow-drop: #{$alpha-color-black-3};
+ --shadow-box: #{$COLOR_LIGHT_BLACK_2};
+ // States
+ --state-normal: #{$color-white};
+ --state-hover: #{$color-grey-4};
+ --state-active: #{$color-grey-5};
+ --state-disabled: #{$color-grey-3};
+ --checkbox-disabled-grey: #{$color-grey-6};
+ --sidebar-tab: #{$color-grey-6};
+ // Border
+ --border-normal: #{$color-grey-5};
+ --border-normal-1: #{$color-grey-5};
+ --border-normal-2: #{$color-grey-5};
+ --border-normal-3: #{$color-grey-6};
+ --border-hover: #{$color-grey-1};
+ --border-hover-1: #{$color-black-9};
+ --border-active: var(--brand-secondary);
+ --border-disabled: #{$color-grey-3};
+ --border-divider: #{$color-grey-2};
+ // Fill
+ --fill-normal: #{$color-white};
+ --fill-normal-1: #{$color-grey};
+ --fill-hover: #{$color-grey-1};
+ --fill-active: var(--brand-secondary);
+ --fill-disabled: #{$color-grey-3};
+ // Status
+ --status-default: #{$color-grey-3};
+ --status-adjustment: #{$color-grey-1};
+ --status-danger: #{$color-red-1};
+ --status-success: #{$color-green-1};
+ --status-warning: #{$color-yellow};
+ --status-warning-transparent: #{$alpha-color-yellow-1};
+ --status-transfer: #{$color-orange};
+ --status-info: #{$color-blue};
+ --status-colored-background: #{$color-white};
+ --status-alert-background: #{$color-yellow-3};
+ // Dashboard
+ --dashboard-swap-free: #{$gradient-color-black-3};
+ --dashboard-app: #{$gradient-color-black-4};
+ // Payment methods
+ --payment-skrill: #{$gradient-color-purple};
+ --payment-visa: #{$gradient-color-blue};
+ --payment-mastercard: #{$gradient-color-orange};
+ --payment-credit-and-debit: #{$gradient-color-black-2};
+ --payment-bitcoin: #{$gradient-color-orange-2};
+ --payment-usd-coin: #{$gradient-color-blue-2};
+ --payment-neteller: #{$gradient-color-green-3};
+ --payment-webmoney: #{$gradient-color-blue-3};
+ --payment-jeton: #{$gradient-color-orange-3};
+ --payment-sticpay: #{$gradient-color-orange-4};
+ --payment-airtm: #{$gradient-color-blue-4};
+ --payment-dp2p: #{$gradient-color-red-3};
+ --payment-deriv: #{$gradient-color-red-4};
+ // 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};
+ --transparent-correct-message: #{$color-green-5};
+ --transparent-fake-message: #{$color-red-6};
+ /* TODO: change to styleguide later */
+ // Gradient
+ --gradient-success: #{$gradient-color-green-1};
+ --gradient-danger: #{$gradient-color-red-1};
+ --contract-gradient-danger: #{$contract-gradient-color-red-1};
+ --gradient-right-edge: #{$gradient-color-white};
+ --gradient-blue: #{$gradient-color-blue-5};
+ --gradient-gold: #{$gradient-color-gold};
+ --gradient-green: #{$gradient-color-green-4};
+ // Badge
+ --badge-white: #{$color-white};
+ --badge-blue: #{$color-blue-4};
+ --badge-violet: #{$color-blue-2};
+ --badge-green: #{$color-green-3};
+ //TradersHub Banner
+ --traders-hub-banner-border-color: #{$color-grey-4};
+ // wallets
+ --wallets-banner-ready-bg-color: #{$ready-banner-bg-color};
+ --wallets-banner-ready-tick-bg-color: #{$ready-banner-tick-bg-color};
+ --wallets-banner-border-color: #{$color-grey-4};
+ --wallets-banner-dot-color: #{$color-grey-6};
+ --wallets-banner-active-dot-color: #{$color-red};
+ --wallets-card-active-gradient-background: #{$gradient-color-black-5};
+ --wallet-demo-bg-color: #{$wallet-demo-bg-color};
+ --wallet-demo-divider-color: #{$wallet-demo-divider-color};
+ --wallet-eu-disclaimer: #{$color-grey-4};
+ --wallet-box-shadow: #{$wallet-box-shadow};
+ // Demo view
+ --demo-text-color-1: #{$color-grey};
+ --demo-text-color-2: #{$color-white};
+ // Header
+ --header-background-mt5: #{$color-blue-8};
+ --header-background-others: #{$color-green-7};
+ }
+ .theme--dark {
+ // General
+ --general-main-1: #{$color-black};
+ --general-main-2: #{$color-black-3};
+ --general-main-3: #{$color-black-4};
+ --general-main-4: #{$alpha-color-black-7};
+ --general-section-1: #{$color-black-3};
+ --general-section-2: #{$color-black};
+ --general-section-3: #{$color-black-5};
+ // @TODO: get color from design
+ --general-section-4: #{$color-black-5};
+ --general-section-5: #{$color-black-5};
+ --general-section-6: #{$color-grey-7};
+ --general-disabled: #{$color-black-4};
+ --general-hover: #{$color-black-5};
+ --general-active: #{$color-black-8};
+ // Icons and Texts
+ --text-prominent: #{$color-white};
+ --text-general: #{$color-grey};
+ --text-less-prominent: #{$color-grey-7};
+ --text-primary: #{$color-grey-1};
+ --text-disabled: #{$color-black-6};
+ --text-disabled-1: #{$color-black-6};
+ --text-profit-success: #{$color-green-3};
+ --text-loss-danger: #{$color-red-2};
+ --text-red: #{$color-red};
+ --text-colored-background: #{$color-white};
+ --text-info-blue: #{$color-blue-6};
+ --text-info-blue-background: #{$color-blue-7};
+ --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};
+ --icon-black-plus: #{$color-white};
+ // Purchase
+ --purchase-main-1: #{$color-green-3};
+ --purchase-section-1: #{$color-green-4};
+ --purchase-main-2: #{$color-red-2};
+ --purchase-section-2: #{$color-red-3};
+ --purchase-disabled-main: #{$color-black-4};
+ --purchase-disabled-section: #{$color-black};
+ --button-primary-default: var(--brand-red-coral);
+ --button-secondary-default: #{$color-grey-7};
+ --button-tertiary-default: transparent;
+ --button-primary-light-default: #{$alpha-color-red-2};
+ --button-primary-hover: #{$color-red-5};
+ --button-secondary-hover: #{$alpha-color-white-3};
+ --button-tertiary-hover: #{$alpha-color-red-1};
+ --button-primary-light-hover: #{$alpha-color-red-3};
+ --button-toggle-primary: #{$color-blue-3};
+ --button-toggle-secondary: #{$color-black-8};
+ --button-toggle-alternate: #{$color-black-8};
+ // Overlay
+ --overlay-outside-dialog: #{$alpha-color-black-1};
+ --overlay-inside-dialog: #{$alpha-color-black-2};
+ // Shadow
+ --shadow-menu: #{$alpha-color-black-5};
+ --shadow-menu-2: #{$alpha-color-black-1};
+ --shadow-drop: #{$alpha-color-black-6};
+ --shadow-box: #{$COLOR_DARK_GRAY_3};
+ // States
+ --state-normal: #{$color-black};
+ --state-hover: #{$color-black-5};
+ --state-active: #{$color-black-8};
+ --state-disabled: #{$color-black-4};
+ --checkbox-disabled-grey: #{$color-grey-6};
+ --sidebar-tab: #{$color-grey-7};
+ // Border
+ --border-normal: #{$color-black-8};
+ --border-normal-1: #{$color-grey-5};
+ --border-normal-2: #{$color-grey-1};
+ --border-normal-3: #{$color-grey-7};
+ --border-hover: #{$color-grey-7};
+ --border-hover-1: #{$color-black-9};
+ --border-active: var(--brand-secondary);
+ --border-disabled: #{$color-black-4};
+ --border-divider: #{$color-grey-13};
+ // Fill
+ --fill-normal: #{$color-black};
+ --fill-normal-1: #{$color-black-1};
+ --fill-hover: #{$color-grey-7};
+ --fill-active: var(--brand-secondary);
+ --fill-disabled: #{$color-black-4};
+ // Status
+ --status-default: #{$color-grey-3};
+ --status-adjustment: #{$color-grey-1};
+ --status-danger: #{$color-red-2};
+ --status-warning: #{$color-yellow};
+ --status-warning-transparent: #{$alpha-color-yellow-1};
+ --status-success: #{$color-green-3};
+ --status-transfer: #{$color-orange};
+ --status-info: #{$color-blue};
+ --status-colored-background: #{$color-white};
+ --status-alert-background: #{$color-yellow-3};
+ // 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};
+ --transparent-correct-message: #{$color-green-5};
+ --transparent-fake-message: #{$color-red-6};
+ /* TODO: change to styleguide later */
+ // Gradient
+ --gradient-success: #{$gradient-color-green-2};
+ --gradient-danger: #{$gradient-color-red-2};
+ --contract-gradient-danger: #{$contract-gradient-color-red-2};
+ --gradient-right-edge: #{$gradient-color-black};
+ --gradient-blue: #{$gradient-color-blue-5};
+ --gradient-gold: #{$gradient-color-gold};
+ --gradient-green: #{$gradient-color-green-4};
+ // Badge
+ --badge-white: #{$color-white};
+ --badge-blue: #{$color-blue-4};
+ --badge-violet: #{$color-blue-2};
+ --badge-green: #{$color-green-3};
+ //TradersHub Banner
+ --traders-hub-banner-border-color: #{$color-black-5};
+ // wallets
+ --wallets-banner-ready-bg-color: #{$ready-banner-bg-color};
+ --wallets-banner-ready-tick-bg-color: #{$ready-banner-tick-bg-color};
+ --wallets-banner-border-color: #{$color-grey-4};
+ --wallets-banner-dot-color: #{$color-grey-6};
+ --wallets-banner-active-dot-color: #{$color-red};
+ --wallets-card-active-gradient-background: #{$gradient-color-white-2};
+ --wallet-demo-bg-color: #{$wallet-dark-demo-bg-color};
+ --wallet-demo-divider-color: #{$color-black-8};
+ --wallet-eu-disclaimer: #{$color-grey-4};
+ --wallet-box-shadow: #{$wallet-box-shadow};
+ // Demo view
+ --demo-text-color-1: #{$color-black-1};
+ --demo-text-color-2: #{$color-black-1};
+ // Header
+ --header-background-mt5: #{$color-blue-8};
+ --header-background-others: #{$color-green-7};
+ }
+}
diff --git a/src/components/shared/test_utils/test_common.ts b/src/components/shared/test_utils/test_common.ts
new file mode 100644
index 00000000..43d90156
--- /dev/null
+++ b/src/components/shared/test_utils/test_common.ts
@@ -0,0 +1,19 @@
+/* eslint-disable import/no-import-module-exports */
+import { JSDOM } from 'jsdom';
+
+import { reset } from '../utils/url/url';
+
+const jsdom = new JSDOM();
+
+const setURL = (url: string) => {
+ jsdom.reconfigure({ url });
+ // eslint-disable-next-line no-unused-expressions
+ jsdom.window.location.href = url;
+ reset();
+};
+
+module.exports = {
+ expect,
+ setURL,
+ getApiToken: () => 'hhh9bfrbq0G3dRf',
+};
diff --git a/src/components/shared/utils/array/array.ts b/src/components/shared/utils/array/array.ts
new file mode 100644
index 00000000..d1beef79
--- /dev/null
+++ b/src/components/shared/utils/array/array.ts
@@ -0,0 +1,15 @@
+export const shuffleArray = (array: T[]): T[] => {
+ const firstDigit = (num: number) => Number(String(num)[0]);
+
+ for (let i = array.length - 1; i > 0; i--) {
+ const crypto = window.crypto || (window as any).msCrypto; // to make it working in MS Explorer
+ const random_array = new Uint32Array(1);
+ const random_number = crypto.getRandomValues(random_array);
+ const j = Math.floor((firstDigit(random_number[0]) / 10) * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array;
+};
+
+// @ts-expect-error as the generic is a Array
+export const flatten = >(arr: T) => [].concat(...arr);
diff --git a/src/components/shared/utils/array/index.ts b/src/components/shared/utils/array/index.ts
new file mode 100644
index 00000000..bd9a11d9
--- /dev/null
+++ b/src/components/shared/utils/array/index.ts
@@ -0,0 +1 @@
+export * from './array';
diff --git a/src/components/shared/utils/brand/brand.ts b/src/components/shared/utils/brand/brand.ts
new file mode 100644
index 00000000..aaeef85c
--- /dev/null
+++ b/src/components/shared/utils/brand/brand.ts
@@ -0,0 +1,76 @@
+import config_data from '../../brand.config.json';
+
+type TLandingCompany = {
+ fx: string;
+ malta: string;
+ maltainvest: string;
+ mx: string;
+ samoa: string;
+ svg: string;
+ v: string;
+};
+
+type TPlatform = {
+ name: string;
+ icon: string;
+};
+
+type TPlatformAppstore = {
+ name: string;
+ icon: string;
+ availability: string;
+};
+
+type TPlatforms = {
+ ctrader: TPlatform;
+ trader: TPlatform;
+ dbot: TPlatform;
+ mt5: TPlatform;
+ dxtrade: TPlatform;
+ smarttrader: TPlatform;
+ bbot: TPlatform;
+ go: TPlatform;
+};
+
+type TPlatformsAppstore = {
+ ctrader: TPlatformAppstore;
+ trader: TPlatformAppstore;
+ dbot: TPlatformAppstore;
+ smarttrader: TPlatformAppstore;
+ bbot: TPlatformAppstore;
+ go: TPlatformAppstore;
+};
+
+const isDomainAllowed = (domain_name: string) => {
+ // This regex will match any official deriv production and testing domain names.
+ // Allowed deriv domains: localhost, binary.sx, binary.com, deriv.com, deriv.be, deriv.me and their subdomains.
+ return /^(((.*)\.)?(localhost:8443|pages.dev|binary\.(sx|com)|deriv.(com|me|be|dev)))$/.test(domain_name);
+};
+
+export const getLegalEntityName = (landing_company: keyof TLandingCompany) => {
+ return config_data.legal_entities[landing_company];
+};
+
+export const getBrandWebsiteName = () => {
+ return config_data.domain_name;
+};
+
+export const getPlatformSettings = (platform_key: keyof TPlatforms): TPlatform => {
+ const allowed_config_data = config_data.platforms[platform_key];
+
+ if (!isDomainAllowed(window.location.host)) {
+ // Remove all official platform logos if the app is hosted under unofficial domain
+ allowed_config_data.icon = '';
+ }
+
+ return allowed_config_data;
+};
+
+export const getAppstorePlatforms = () => {
+ const platform_data: Record> = config_data.platforms_appstore;
+ return Object.keys(platform_data).map(key => platform_data[key]);
+};
+
+export const getPlatformSettingsAppstore = (platform_key: keyof TPlatformsAppstore): TPlatformAppstore => {
+ return config_data.platforms_appstore[platform_key];
+};
diff --git a/src/components/shared/utils/brand/index.ts b/src/components/shared/utils/brand/index.ts
new file mode 100644
index 00000000..095cd25b
--- /dev/null
+++ b/src/components/shared/utils/brand/index.ts
@@ -0,0 +1 @@
+export * from './brand';
diff --git a/src/components/shared/utils/browser/browser_detect.ts b/src/components/shared/utils/browser/browser_detect.ts
new file mode 100644
index 00000000..73372922
--- /dev/null
+++ b/src/components/shared/utils/browser/browser_detect.ts
@@ -0,0 +1,33 @@
+declare global {
+ interface Window {
+ safari?: { pushNotification: () => void };
+ HTMLElement: HTMLElement & string;
+ }
+}
+
+export const isSafari = () => {
+ return (
+ /constructor/i.test(window.HTMLElement) ||
+ (function (p) {
+ return p.toString() === '[object SafariRemoteNotification]';
+ })(!window.safari || (typeof window.safari !== 'undefined' && window.safari.pushNotification))
+ );
+};
+
+const getUserBrowser = () => {
+ // We can't rely only on navigator.userAgent.index, the verification order is also important
+ if ((navigator.userAgent.indexOf('Opera') || navigator.userAgent.indexOf('OPR')) !== -1) {
+ return 'Opera';
+ } else if (navigator.userAgent.indexOf('Edg') !== -1) {
+ return 'Edge';
+ } else if (navigator.userAgent.indexOf('Chrome') !== -1) {
+ return 'Chrome';
+ } else if (navigator.userAgent.indexOf('Safari') !== -1) {
+ return 'Safari';
+ } else if (navigator.userAgent.indexOf('Firefox') !== -1) {
+ return 'Firefox';
+ }
+ return 'unknown';
+};
+
+export const isSafariBrowser = () => getUserBrowser() === 'Safari';
diff --git a/src/components/shared/utils/browser/index.ts b/src/components/shared/utils/browser/index.ts
new file mode 100644
index 00000000..75bc29ed
--- /dev/null
+++ b/src/components/shared/utils/browser/index.ts
@@ -0,0 +1 @@
+export * from './browser_detect';
diff --git a/src/components/shared/utils/cfd/available-cfd-accounts.ts b/src/components/shared/utils/cfd/available-cfd-accounts.ts
new file mode 100644
index 00000000..a911c168
--- /dev/null
+++ b/src/components/shared/utils/cfd/available-cfd-accounts.ts
@@ -0,0 +1,42 @@
+import { localize } from '@/utils/tmp/dummy';
+
+import { CFD_PLATFORMS } from '../platform';
+
+export interface AvailableAccount {
+ name: string;
+ description?: string;
+ is_visible?: boolean;
+ is_disabled?: boolean;
+ platform?: string;
+ market_type?: 'all' | 'financial' | 'synthetic';
+ icon: string;
+ availability: string;
+ link_to?: string;
+}
+
+export const getCFDAvailableAccount = () => [
+ {
+ name: 'Derived',
+ description: localize('CFDs on derived instruments.'),
+ platform: CFD_PLATFORMS.MT5,
+ market_type: 'synthetic',
+ icon: 'Derived',
+ availability: 'Non-EU',
+ },
+ {
+ name: 'Deriv X',
+ description: localize('CFDs on financial and derived instruments via a customisable platform.'),
+ platform: CFD_PLATFORMS.DXTRADE,
+ market_type: 'all',
+ icon: 'DerivX',
+ availability: 'Non-EU',
+ },
+ {
+ name: 'Deriv cTrader',
+ description: localize('CFDs on financial and derived instruments with copy trading.'),
+ platform: CFD_PLATFORMS.CTRADER,
+ market_type: 'all',
+ icon: 'CTrader',
+ availability: 'Non-EU',
+ },
+];
diff --git a/src/components/shared/utils/cfd/cfd.ts b/src/components/shared/utils/cfd/cfd.ts
new file mode 100644
index 00000000..50991100
--- /dev/null
+++ b/src/components/shared/utils/cfd/cfd.ts
@@ -0,0 +1,523 @@
+import { DetailsOfEachMT5Loginid, GetAccountStatus, LandingCompany } from '@deriv/api-types';
+
+import { localize } from '@/utils/tmp/dummy';
+
+import { AUTH_STATUS_CODES, Jurisdiction, JURISDICTION_MARKET_TYPES } from '../constants';
+import { CFD_PLATFORMS } from '../platform';
+
+let CFD_text_translated: { [key: string]: () => void };
+
+export const CFD_text: { [key: string]: string } = {
+ dxtrade: 'Deriv X',
+ mt5: 'MT5',
+ mt5_cfds: 'MT5 CFDs',
+ cfd: 'CFDs',
+ ctrader: 'Deriv cTrader',
+ synthetic: 'Derived',
+ synthetic_demo: 'Derived Demo',
+ synthetic_bvi: 'Derived BVI',
+ synthetic_svg: 'Derived SVG',
+ synthetic_v: 'Derived Vanuatu',
+ financial: 'Financial',
+ financial_demo: 'Financial Demo',
+ financial_bvi: 'Financial BVI',
+ financial_fx: 'Financial Labuan',
+ financial_v: 'Financial Vanuatu',
+ financial_svg: 'Financial SVG',
+ all: 'Swap-Free',
+ all_demo: 'Swap-Free Demo',
+ all_svg: 'Swap-Free SVG',
+} as const;
+
+export const getMT5Title = (account_type: string) => {
+ if (account_type === 'synthetic') {
+ return CFD_text.synthetic;
+ }
+ if (account_type === 'all') {
+ return CFD_text.all;
+ }
+ return CFD_text.financial;
+};
+
+type TPlatform = 'dxtrade' | 'mt5' | 'ctrader';
+type TMarketType = 'financial' | 'synthetic' | 'gaming' | 'all' | undefined;
+type TShortcode = 'svg' | 'bvi' | 'labuan' | 'vanuatu' | 'malta' | 'maltainvest';
+type TGetAccount = {
+ market_type: TMarketType;
+ sub_account_type?: TAccount['sub_account_type'];
+ platform: TPlatform;
+};
+type TGetCFDAccountKey = TGetAccount & {
+ shortcode?: TShortcode;
+};
+
+// * mt5_login_list returns these:
+// market_type: "financial" | "gaming"
+// sub_account_type: "financial" | "financial_stp" | "swap_free"
+// *
+// sub_account_type financial_stp only happens in "financial" market_type
+// dxrade and swap_free both have market_type "all" so check for platform is neccessary
+export const getCFDAccountKey = ({ market_type, sub_account_type, platform, shortcode }: TGetCFDAccountKey) => {
+ if (platform === CFD_PLATFORMS.MT5 && market_type === 'all') {
+ // currently we are only supporting SVG for SwapFree
+ switch (shortcode) {
+ case 'svg':
+ return 'all_svg';
+ default:
+ return 'all_demo';
+ }
+ }
+ if (market_type === 'all') {
+ switch (platform) {
+ case CFD_PLATFORMS.CTRADER:
+ return 'ctrader';
+ default:
+ return 'dxtrade';
+ }
+ }
+
+ if (market_type === 'gaming' || market_type === 'synthetic') {
+ if (platform === CFD_PLATFORMS.DXTRADE || sub_account_type === 'financial') {
+ switch (shortcode) {
+ case 'svg':
+ return 'synthetic_svg';
+ case 'bvi':
+ return 'synthetic_bvi';
+ case 'vanuatu':
+ return 'synthetic_v';
+ default:
+ return 'synthetic_demo';
+ }
+ }
+ }
+ if (market_type === 'financial') {
+ if (
+ platform === CFD_PLATFORMS.DXTRADE ||
+ sub_account_type === 'financial' ||
+ sub_account_type === 'financial_stp'
+ ) {
+ switch (shortcode) {
+ case 'svg':
+ return 'financial_svg';
+ case 'bvi':
+ return 'financial_bvi';
+ case 'labuan':
+ return 'financial_fx';
+ case 'vanuatu':
+ return 'financial_v';
+ case 'maltainvest':
+ return 'financial';
+ default:
+ return 'financial_demo';
+ }
+ }
+ }
+ return undefined;
+};
+
+/**
+ * Generate the enum for API request.
+ *
+ * @param {string} category [real, demo]
+ * @param {string} type [synthetic, financial, financial_stp]
+ * @return {string}
+ */
+
+type TGetAccountTypeFields = {
+ category: 'real' | 'demo';
+ type: 'financial' | 'synthetic' | 'all';
+};
+
+type TAccountType = {
+ account_type: string;
+ mt5_account_type?: string;
+};
+
+type TAccountTypes = Record;
+
+type TMapMode = Record;
+
+export const getAccountTypeFields = ({ category, type }: TGetAccountTypeFields) => {
+ const map_mode: TMapMode = {
+ real: {
+ synthetic: {
+ account_type: 'gaming',
+ },
+ financial: {
+ account_type: 'financial',
+ mt5_account_type: 'financial',
+ },
+ all: {
+ account_type: 'all',
+ },
+ },
+ demo: {
+ synthetic: {
+ account_type: 'demo',
+ },
+ financial: {
+ account_type: 'demo',
+ mt5_account_type: 'financial',
+ },
+ all: {
+ account_type: 'demo',
+ },
+ },
+ };
+
+ return map_mode[category][type];
+};
+
+type TGetCFDAccountDisplay = TGetCFDAccountKey & {
+ is_eu: boolean;
+ is_mt5_trade_modal?: boolean;
+ is_transfer_form?: boolean;
+};
+
+export const getCFDAccountDisplay = ({
+ market_type,
+ sub_account_type,
+ platform,
+ is_eu,
+ shortcode,
+ is_mt5_trade_modal,
+ is_transfer_form = false,
+}: TGetCFDAccountDisplay) => {
+ const cfd_account_key = getCFDAccountKey({ market_type, sub_account_type, platform, shortcode });
+ if (!cfd_account_key) return undefined;
+
+ if (is_mt5_trade_modal && is_eu) {
+ switch (cfd_account_key) {
+ case 'financial':
+ return localize('CFDs');
+ case 'financial_demo':
+ default:
+ return localize('CFDs Demo');
+ }
+ }
+
+ const cfd_account_display = CFD_text_translated[cfd_account_key]();
+
+ // TODO condition will be changed when card 74063 is merged
+ if (market_type === 'synthetic' && platform === CFD_PLATFORMS.DXTRADE) return localize('Synthetic');
+ if (market_type === 'all' && platform === CFD_PLATFORMS.DXTRADE && is_transfer_form) return '';
+ if (market_type === 'all' && platform === CFD_PLATFORMS.CTRADER && is_transfer_form) return '';
+ if (platform === CFD_PLATFORMS.CTRADER) return cfd_account_display;
+
+ return cfd_account_display;
+};
+
+type TGetCFDAccount = TGetAccount & {
+ is_eu?: boolean;
+ is_transfer_form?: boolean;
+};
+
+type TGetMT5Icon = {
+ market_type: TMarketType;
+ is_eu?: boolean;
+};
+
+export const getCFDAccount = ({
+ market_type,
+ sub_account_type,
+ platform,
+ is_eu,
+ is_transfer_form = false,
+}: TGetCFDAccount) => {
+ let cfd_account_key = getCFDAccountKey({ market_type, sub_account_type, platform });
+ if (!cfd_account_key) return undefined;
+
+ if (cfd_account_key === 'financial_demo' && is_eu) {
+ cfd_account_key = 'cfd';
+ }
+
+ if (cfd_account_key === 'ctrader' && is_transfer_form) return 'Ctrader';
+
+ return CFD_text[cfd_account_key as keyof typeof CFD_text];
+};
+
+export const getMT5Icon = ({ market_type, is_eu }: TGetMT5Icon) => {
+ if (market_type === 'all' && !is_eu) return 'SwapFree';
+ if (market_type === 'financial' && is_eu) return 'CFDs';
+ return market_type;
+};
+
+export const setSharedCFDText = (all_shared_CFD_text: { [key: string]: () => void }) => {
+ CFD_text_translated = all_shared_CFD_text;
+};
+
+type TAccount = DetailsOfEachMT5Loginid & { platform: string };
+export const getAccountListKey = (account: TAccount, platform: TPlatform, shortcode?: TShortcode) => {
+ return `${account.platform || platform}.${account.account_type}.${getCFDAccountKey({
+ market_type: account.market_type,
+ sub_account_type: account.sub_account_type,
+ platform,
+ shortcode,
+ })}@${
+ platform === CFD_PLATFORMS.DXTRADE || platform === CFD_PLATFORMS.CTRADER ? account.market_type : account.server
+ }`;
+};
+
+export const getCFDPlatformLabel = (platform: TPlatform) => {
+ switch (platform) {
+ case CFD_PLATFORMS.MT5:
+ return 'Deriv MT5';
+ case CFD_PLATFORMS.DXTRADE:
+ return 'Deriv X';
+ case CFD_PLATFORMS.CTRADER:
+ return 'Deriv cTrader';
+ default:
+ return '';
+ }
+};
+
+export const getCFDPlatformNames = (platform: TPlatform) => {
+ switch (platform) {
+ case CFD_PLATFORMS.MT5:
+ return 'MT5';
+ case CFD_PLATFORMS.DXTRADE:
+ return 'Deriv X';
+ case CFD_PLATFORMS.CTRADER:
+ return 'cTrader';
+ default:
+ return '';
+ }
+};
+
+type TIsLandingCompanyEnabled = {
+ landing_companies: LandingCompany;
+ platform: TPlatform;
+ type: TMarketType | 'financial_stp';
+};
+
+export const isLandingCompanyEnabled = ({ landing_companies, platform, type }: TIsLandingCompanyEnabled) => {
+ if (platform === CFD_PLATFORMS.MT5) {
+ if (type === 'gaming') return !!landing_companies?.mt_gaming_company?.financial;
+ if (type === 'financial') return !!landing_companies?.mt_financial_company?.financial;
+ if (type === 'financial_stp') return !!landing_companies?.mt_financial_company?.financial_stp;
+ } else if (platform === CFD_PLATFORMS.DXTRADE) {
+ if (type === 'all') return !!landing_companies?.dxtrade_all_company?.standard;
+ if (type === 'gaming') return !!landing_companies?.dxtrade_gaming_company?.standard;
+ if (type === 'financial') return !!landing_companies?.dxtrade_financial_company?.standard;
+ }
+ return false;
+};
+
+// Define the AuthenticationStatusInfo type
+type TAuthenticationStatusInfo = {
+ poa_status?: string;
+ poi_status?: string;
+ idv_status?: string;
+ onfido_status?: string;
+ manual_status?: string;
+ acknowledged_status: string[];
+ poi_acknowledged_for_maltainvest: boolean;
+ poi_poa_verified_for_bvi_labuan_vanuatu: boolean;
+ poa_acknowledged: boolean;
+ poi_poa_verified_for_maltainvest: boolean;
+ need_poa_submission: boolean;
+ poi_verified_for_maltainvest: boolean;
+ poi_acknowledged_for_bvi_labuan_vanuatu: boolean;
+ poi_verified_for_bvi_labuan_vanuatu: boolean;
+ poa_verified: boolean;
+ poi_or_poa_not_submitted: boolean;
+ need_poa_resubmission: boolean;
+ poi_and_poa_not_submitted: boolean;
+ poa_not_submitted: boolean;
+ poi_not_submitted: boolean;
+ need_poi_for_maltainvest: boolean;
+ need_poi_for_bvi_labuan_vanuatu: boolean;
+ poi_not_submitted_for_maltainvest: boolean;
+ poi_pending_for_bvi_labuan_vanuatu: boolean;
+ poi_pending_for_maltainvest: boolean;
+ poi_resubmit_for_maltainvest: boolean;
+ poi_resubmit_for_bvi_labuan_vanuatu: boolean;
+ poa_pending: boolean;
+ is_idv_revoked: boolean;
+};
+
+export const getAuthenticationStatusInfo = (account_status: GetAccountStatus): TAuthenticationStatusInfo => {
+ const risk_classification = account_status?.risk_classification;
+
+ const poa_status: string = account_status?.authentication?.document?.status || '';
+ const poi_status: string = account_status?.authentication?.identity?.status || '';
+
+ const services = account_status?.authentication?.identity?.services ?? {};
+ const {
+ idv: { status: idv_status } = {},
+ onfido: { status: onfido_status } = {},
+ manual: { status: manual_status } = {},
+ } = services;
+
+ const is_idv_revoked = account_status?.status?.includes('idv_revoked');
+
+ const acknowledged_status: string[] = ['pending', 'verified'];
+ const failed_cases: string[] = ['rejected', 'expired', 'suspected'];
+
+ const poa_not_submitted: boolean = poa_status === 'none';
+ const need_poa_submission = !acknowledged_status.includes(poa_status);
+ const need_poa_resubmission: boolean = failed_cases.includes(poa_status);
+ const poa_verified: boolean = poa_status === 'verified';
+ const poa_pending: boolean = poa_status === 'pending';
+ const poa_acknowledged: boolean = acknowledged_status.includes(poa_status);
+
+ const poi_not_submitted: boolean = poi_status === 'none';
+ const poi_or_poa_not_submitted: boolean = poa_not_submitted || poi_not_submitted;
+ const poi_and_poa_not_submitted: boolean = poa_not_submitted && poi_not_submitted;
+
+ //maltainvest
+
+ // mf = maltainvest: only require onfido and manual
+ const mf_jurisdiction_statuses: string[] = [onfido_status, manual_status].filter(
+ (status: string | undefined) => status
+ ) as string[];
+ // bvi_labuan_vanuatu jurisdictions: require idv, onfido and manual
+ const bvi_labuan_vanuatu_jurisdiction_statuses: string[] = [idv_status, onfido_status, manual_status].filter(
+ status => status
+ ) as string[];
+
+ const poi_verified_for_maltainvest: boolean = mf_jurisdiction_statuses.includes('verified');
+ const poi_acknowledged_for_maltainvest: boolean = mf_jurisdiction_statuses.some(status =>
+ acknowledged_status.includes(status)
+ );
+ const poi_pending_for_maltainvest: boolean =
+ mf_jurisdiction_statuses.some(status => status === 'pending') && !poi_verified_for_maltainvest;
+
+ const need_poi_for_maltainvest = !poi_acknowledged_for_maltainvest;
+ const poi_not_submitted_for_maltainvest: boolean = mf_jurisdiction_statuses.every(status => status === 'none');
+
+ const poi_resubmit_for_maltainvest: boolean =
+ !poi_pending_for_maltainvest && !poi_not_submitted_for_maltainvest && !poi_verified_for_maltainvest;
+
+ const poi_poa_verified_for_maltainvest = poi_verified_for_maltainvest && poa_verified;
+
+ //bvi-labuan-vanuatu
+ let poi_acknowledged_for_bvi_labuan_vanuatu: boolean = bvi_labuan_vanuatu_jurisdiction_statuses.some(status =>
+ acknowledged_status.includes(status)
+ );
+ if (risk_classification === 'high') {
+ poi_acknowledged_for_bvi_labuan_vanuatu = Boolean(onfido_status && acknowledged_status.includes(onfido_status));
+ } else {
+ poi_acknowledged_for_bvi_labuan_vanuatu = bvi_labuan_vanuatu_jurisdiction_statuses.some(status =>
+ acknowledged_status.includes(status)
+ );
+ }
+ const need_poi_for_bvi_labuan_vanuatu = !poi_acknowledged_for_bvi_labuan_vanuatu;
+ const poi_not_submitted_for_bvi_labuan_vanuatu: boolean = bvi_labuan_vanuatu_jurisdiction_statuses.every(
+ status => status === 'none'
+ );
+
+ const poi_verified_for_bvi_labuan_vanuatu: boolean = bvi_labuan_vanuatu_jurisdiction_statuses.includes('verified');
+
+ const poi_pending_for_bvi_labuan_vanuatu: boolean =
+ bvi_labuan_vanuatu_jurisdiction_statuses.includes('pending') && !poi_verified_for_bvi_labuan_vanuatu;
+
+ const poi_resubmit_for_bvi_labuan_vanuatu: boolean =
+ !poi_pending_for_bvi_labuan_vanuatu &&
+ !poi_not_submitted_for_bvi_labuan_vanuatu &&
+ !poi_verified_for_bvi_labuan_vanuatu;
+
+ const poi_poa_verified_for_bvi_labuan_vanuatu: boolean = poi_verified_for_bvi_labuan_vanuatu && poa_verified;
+
+ return {
+ poa_status,
+ poi_status,
+ idv_status,
+ onfido_status,
+ manual_status,
+ acknowledged_status,
+ poi_acknowledged_for_maltainvest,
+ poi_poa_verified_for_bvi_labuan_vanuatu,
+ poa_acknowledged,
+ poi_poa_verified_for_maltainvest,
+ need_poa_submission,
+ poi_verified_for_maltainvest,
+ poi_acknowledged_for_bvi_labuan_vanuatu,
+ poi_verified_for_bvi_labuan_vanuatu,
+ poa_verified,
+ poi_or_poa_not_submitted,
+ need_poa_resubmission,
+ poi_and_poa_not_submitted,
+ poa_not_submitted,
+ poi_not_submitted,
+ need_poi_for_maltainvest,
+ need_poi_for_bvi_labuan_vanuatu,
+ poi_not_submitted_for_maltainvest,
+ poi_pending_for_bvi_labuan_vanuatu,
+ poi_pending_for_maltainvest,
+ poi_resubmit_for_maltainvest,
+ poi_resubmit_for_bvi_labuan_vanuatu,
+ poa_pending,
+ is_idv_revoked,
+ };
+};
+
+export const mt5_community_url =
+ 'https://community.deriv.com/t/mt5-new-server-name-and-mobile-app-re-login-guide/70617';
+
+export const mt5_help_centre_url = '/help-centre/dmt5/#log-in-to-my-Deriv-MT5-account';
+
+export const getFormattedJurisdictionCode = (jurisdiction_code?: (typeof Jurisdiction)[keyof typeof Jurisdiction]) => {
+ let formatted_label = '';
+
+ switch (jurisdiction_code) {
+ case Jurisdiction.SVG:
+ formatted_label = localize('SVG');
+ break;
+ case Jurisdiction.BVI:
+ formatted_label = localize('BVI');
+ break;
+ case Jurisdiction.LABUAN:
+ formatted_label = localize('Labuan');
+ break;
+ case Jurisdiction.VANUATU:
+ formatted_label = localize('Vanuatu');
+ break;
+ case Jurisdiction.MALTA_INVEST:
+ formatted_label = localize('Malta');
+ break;
+ default:
+ break;
+ }
+ return formatted_label;
+};
+
+export const getFormattedJurisdictionMarketTypes = (
+ jurisdiction_market_type: (typeof JURISDICTION_MARKET_TYPES)[keyof typeof JURISDICTION_MARKET_TYPES] | TMarketType
+) => {
+ let formatted_market_type = '';
+
+ switch (jurisdiction_market_type) {
+ case 'synthetic': // need to remove this once we have the correct market type from BE
+ case JURISDICTION_MARKET_TYPES.DERIVED:
+ formatted_market_type = localize('Derived');
+ break;
+ case JURISDICTION_MARKET_TYPES.FINANCIAL:
+ formatted_market_type = localize('Financial');
+ break;
+ default:
+ break;
+ }
+ return formatted_market_type;
+};
+
+type TGetMT5AccountTitle = {
+ account_type: (typeof JURISDICTION_MARKET_TYPES)[keyof typeof JURISDICTION_MARKET_TYPES];
+ jurisdiction: (typeof Jurisdiction)[keyof typeof Jurisdiction];
+};
+
+//returns the title for the MT5 account - e.g. MT5 Financial Vanuatu
+export const getMT5AccountTitle = ({ account_type, jurisdiction }: TGetMT5AccountTitle) => {
+ return `${getCFDPlatformNames(CFD_PLATFORMS.MT5)} ${getFormattedJurisdictionMarketTypes(
+ account_type
+ )} ${getFormattedJurisdictionCode(jurisdiction)}`;
+};
+
+export const isPOARequiredForMT5 = (account_status: GetAccountStatus, jurisdiction_shortcode: string) => {
+ const { document } = account_status?.authentication || {};
+ if (document?.status === AUTH_STATUS_CODES.PENDING) {
+ return false;
+ }
+ // @ts-expect-error as the prop verified_jurisdiction is not yet present in GetAccountStatu
+ return !document?.verified_jurisdiction[jurisdiction_shortcode];
+};
diff --git a/src/components/shared/utils/cfd/index.ts b/src/components/shared/utils/cfd/index.ts
new file mode 100644
index 00000000..ac3bde7b
--- /dev/null
+++ b/src/components/shared/utils/cfd/index.ts
@@ -0,0 +1,2 @@
+export * from './cfd';
+export * from './available-cfd-accounts';
diff --git a/src/components/shared/utils/config/adapters.ts b/src/components/shared/utils/config/adapters.ts
new file mode 100644
index 00000000..b984b7cd
--- /dev/null
+++ b/src/components/shared/utils/config/adapters.ts
@@ -0,0 +1,40 @@
+import { getIDVNotApplicableOption } from '../constants/default-options';
+
+type TDocument = {
+ id: string;
+ text: string;
+ value?: string;
+ example_format?: string;
+ additional?: {
+ display_name: string;
+ format: string;
+ };
+};
+
+type TIDVFormValues = {
+ document_type: TDocument;
+ document_number: string;
+ document_additional?: string;
+ error_message?: string;
+};
+
+/**
+ * Formats the IDV form values to be sent to the API
+ * @name formatIDVFormValues
+ * @param idv_form_value - Formik values of the IDV form
+ * @param country_code - Country code of the user
+ * @returns IDV form values
+ */
+export const formatIDVFormValues = (idv_form_value: TIDVFormValues, country_code: string) => {
+ const IDV_NOT_APPLICABLE_OPTION = getIDVNotApplicableOption();
+ const idv_submit_data = {
+ document_number:
+ idv_form_value.document_type.id === IDV_NOT_APPLICABLE_OPTION.id
+ ? IDV_NOT_APPLICABLE_OPTION.value
+ : idv_form_value.document_number,
+ document_additional: idv_form_value.document_additional,
+ document_type: idv_form_value.document_type.id,
+ issuing_country: country_code,
+ };
+ return idv_submit_data;
+};
diff --git a/src/components/shared/utils/config/app-config.ts b/src/components/shared/utils/config/app-config.ts
new file mode 100644
index 00000000..f048c5d0
--- /dev/null
+++ b/src/components/shared/utils/config/app-config.ts
@@ -0,0 +1,4 @@
+export const website_domain = 'app.deriv.com';
+export const website_name = 'Deriv';
+export const default_title = website_name;
+export const TRACKING_STATUS_KEY = 'tracking_status';
diff --git a/src/components/shared/utils/config/config.ts b/src/components/shared/utils/config/config.ts
new file mode 100644
index 00000000..6d51850d
--- /dev/null
+++ b/src/components/shared/utils/config/config.ts
@@ -0,0 +1,135 @@
+import { isBot } from '../platform';
+import { isStaging } from '../url/helpers';
+
+/*
+ * Configuration values needed in js codes
+ *
+ * NOTE:
+ * Please use the following command to avoid accidentally committing personal changes
+ * git update-index --assume-unchanged packages/shared/src/utils/config.js
+ *
+ */
+
+export const livechat_license_id = 12049137;
+export const livechat_client_id = '66aa088aad5a414484c1fd1fa8a5ace7';
+
+export const domain_app_ids = {
+ // these domains as supported "production domains"
+ 'deriv.app': 16929, // TODO: [app-link-refactor] - Remove backwards compatibility for `deriv.app`
+ 'app.deriv.com': 16929,
+ 'staging-app.deriv.com': 16303,
+ 'app.deriv.me': 1411,
+ 'staging-app.deriv.me': 1411, // TODO: setup staging for deriv.me
+ 'app.deriv.be': 30767,
+ 'staging-app.deriv.be': 31186,
+ 'binary.com': 1,
+ 'test-app.deriv.com': 51072,
+};
+
+export const platform_app_ids = {
+ derivgo: 23789,
+};
+
+export const getCurrentProductionDomain = () =>
+ !/^staging\./.test(window.location.hostname) &&
+ Object.keys(domain_app_ids).find(domain => window.location.hostname === domain);
+
+export const isProduction = () => {
+ const all_domains = Object.keys(domain_app_ids).map(domain => `(www\\.)?${domain.replace('.', '\\.')}`);
+ return new RegExp(`^(${all_domains.join('|')})$`, 'i').test(window.location.hostname);
+};
+
+export const isTestLink = () => {
+ return /^((.*)\.binary\.sx)$/i.test(window.location.hostname);
+};
+
+export const isLocal = () => /localhost(:\d+)?$/i.test(window.location.hostname);
+
+/**
+ * @deprecated Please use 'WebSocketUtils.getAppId' from '@deriv-com/utils' instead of this.
+ */
+export const getAppId = () => {
+ let app_id = null;
+ const user_app_id = ''; // you can insert Application ID of your registered application here
+ const config_app_id = window.localStorage.getItem('config.app_id');
+ const current_domain = getCurrentProductionDomain() || '';
+ window.localStorage.removeItem('config.platform'); // Remove config stored in localstorage if there's any.
+ const platform = window.sessionStorage.getItem('config.platform');
+ const is_bot = isBot();
+ // Added platform at the top since this should take precedence over the config_app_id
+ if (platform && platform_app_ids[platform as keyof typeof platform_app_ids]) {
+ app_id = platform_app_ids[platform as keyof typeof platform_app_ids];
+ } else if (config_app_id) {
+ app_id = config_app_id;
+ } else if (user_app_id.length) {
+ window.localStorage.setItem('config.default_app_id', user_app_id);
+ app_id = user_app_id;
+ } else if (isStaging()) {
+ window.localStorage.removeItem('config.default_app_id');
+ app_id = is_bot ? 19112 : domain_app_ids[current_domain as keyof typeof domain_app_ids] || 16303; // it's being used in endpoint chrome extension - please do not remove
+ } else if (/localhost/i.test(window.location.hostname)) {
+ app_id = 36300;
+ } else {
+ window.localStorage.removeItem('config.default_app_id');
+ app_id = is_bot ? 19111 : domain_app_ids[current_domain as keyof typeof domain_app_ids] || 16929;
+ }
+
+ return app_id;
+};
+
+export const getSocketURL = () => {
+ const local_storage_server_url = window.localStorage.getItem('config.server_url');
+ if (local_storage_server_url) return local_storage_server_url;
+
+ let active_loginid_from_url;
+ const search = window.location.search;
+ if (search) {
+ const params = new URLSearchParams(document.location.search.substring(1));
+ active_loginid_from_url = params.get('acct1');
+ }
+
+ const loginid = window.localStorage.getItem('active_loginid') || active_loginid_from_url;
+ const is_real = loginid && !/^(VRT|VRW)/.test(loginid);
+
+ const server = is_real ? 'green' : 'blue';
+ const server_url = `${server}.derivws.com`;
+
+ return server_url;
+};
+
+export const checkAndSetEndpointFromUrl = () => {
+ if (isTestLink()) {
+ const url_params = new URLSearchParams(location.search.slice(1));
+
+ if (url_params.has('qa_server') && url_params.has('app_id')) {
+ const qa_server = url_params.get('qa_server') || '';
+ const app_id = url_params.get('app_id') || '';
+
+ url_params.delete('qa_server');
+ url_params.delete('app_id');
+
+ if (/^(^(www\.)?qa[0-9]{1,4}\.deriv.dev|(.*)\.derivws\.com)$/.test(qa_server) && /^[0-9]+$/.test(app_id)) {
+ localStorage.setItem('config.app_id', app_id);
+ localStorage.setItem('config.server_url', qa_server);
+ }
+
+ const params = url_params.toString();
+ const hash = location.hash;
+
+ location.href = `${location.protocol}//${location.hostname}${location.pathname}${
+ params ? `?${params}` : ''
+ }${hash || ''}`;
+
+ return true;
+ }
+ }
+
+ return false;
+};
+
+export const getDebugServiceWorker = () => {
+ const debug_service_worker_flag = window.localStorage.getItem('debug_service_worker');
+ if (debug_service_worker_flag) return !!parseInt(debug_service_worker_flag);
+
+ return false;
+};
diff --git a/src/components/shared/utils/config/index.ts b/src/components/shared/utils/config/index.ts
new file mode 100644
index 00000000..4dc0fbde
--- /dev/null
+++ b/src/components/shared/utils/config/index.ts
@@ -0,0 +1,4 @@
+export * from './config';
+export * from './app-config';
+export * from './platform-config';
+export * from './adapters';
diff --git a/src/components/shared/utils/config/platform-config.ts b/src/components/shared/utils/config/platform-config.ts
new file mode 100644
index 00000000..6fa6c244
--- /dev/null
+++ b/src/components/shared/utils/config/platform-config.ts
@@ -0,0 +1,56 @@
+import React from 'react';
+
+import { initMoment } from '../date';
+import { routes } from '../routes';
+
+type TPlatform = {
+ icon_text?: string;
+ is_hard_redirect: boolean;
+ platform_name: string;
+ route_to_path: string;
+ url?: string;
+};
+
+type TPlatforms = Record<'p2p' | 'derivgo', TPlatform>;
+
+// TODO: This should be moved to PlatformContext
+export const platforms: TPlatforms = {
+ p2p: {
+ icon_text: undefined,
+ is_hard_redirect: true,
+ platform_name: 'Deriv P2P',
+ route_to_path: routes.cashier_p2p,
+ url: 'https://app.deriv.com/cashier/p2p',
+ },
+ derivgo: {
+ icon_text: undefined,
+ is_hard_redirect: true,
+ platform_name: 'Deriv GO',
+ route_to_path: '',
+ url: 'https://app.deriv.com/redirect/derivgo',
+ },
+};
+
+export const useOnLoadTranslation = () => {
+ const [is_loaded, setLoaded] = React.useState(false);
+
+ React.useEffect(() => {
+ if (!i18n.language) {
+ i18n.language = getInitialLanguage();
+ }
+ (async () => {
+ await initMoment(i18n.language);
+ })();
+ const is_english = i18n.language === 'EN';
+ if (is_english) {
+ setLoaded(true);
+ } else {
+ i18n.store.on('added', () => {
+ setLoaded(true);
+ });
+ }
+ return () => i18n.store.off('added');
+ }, []);
+
+ return [is_loaded, setLoaded];
+};
diff --git a/src/components/shared/utils/constants/auth-status-codes.ts b/src/components/shared/utils/constants/auth-status-codes.ts
new file mode 100644
index 00000000..d5f31a06
--- /dev/null
+++ b/src/components/shared/utils/constants/auth-status-codes.ts
@@ -0,0 +1,8 @@
+export const AUTH_STATUS_CODES = {
+ NONE: 'none',
+ PENDING: 'pending',
+ REJECTED: 'rejected',
+ VERIFIED: 'verified',
+ EXPIRED: 'expired',
+ SUSPECTED: 'suspected',
+} as const;
diff --git a/src/components/shared/utils/constants/barriers.ts b/src/components/shared/utils/constants/barriers.ts
new file mode 100644
index 00000000..9dea43de
--- /dev/null
+++ b/src/components/shared/utils/constants/barriers.ts
@@ -0,0 +1,46 @@
+//Configures which trade types have barrier rendered when selected
+export const CONTRACT_SHADES = {
+ ACCU: 'NONE_DOUBLE',
+ CALL: 'ABOVE',
+ PUT: 'BELOW',
+ CALLE: 'ABOVE',
+ PUTE: 'BELOW',
+ EXPIRYRANGE: 'BETWEEN',
+ EXPIRYMISS: 'OUTSIDE',
+ RANGE: 'BETWEEN',
+ UPORDOWN: 'OUTSIDE',
+ ONETOUCH: 'NONE_SINGLE', // no shade
+ NOTOUCH: 'NONE_SINGLE', // no shade
+ ASIANU: 'ABOVE',
+ ASIAND: 'BELOW',
+ MULTUP: 'ABOVE',
+ MULTDOWN: 'BELOW',
+ TURBOSLONG: 'NONE_SINGLE',
+ TURBOSSHORT: 'NONE_SINGLE',
+ VANILLALONGCALL: 'NONE_SINGLE',
+ VANILLALONGPUT: 'NONE_SINGLE',
+ RESETCALL: 'ABOVE',
+ RESETPUT: 'BELOW',
+ LBFLOATPUT: 'NONE_SINGLE',
+ LBFLOATCALL: 'NONE_SINGLE',
+ LBHIGHLOW: 'NONE_DOUBLE',
+} as const;
+
+// Default non-shade according to number of barriers
+export const DEFAULT_SHADES = {
+ 1: 'NONE_SINGLE',
+ 2: 'NONE_DOUBLE',
+};
+
+export const BARRIER_COLORS = {
+ GREEN: '#4bb4b3',
+ RED: '#ec3f3f',
+ ORANGE: '#ff6444',
+ BLUE: '#377cfc',
+};
+
+export const BARRIER_LINE_STYLES = {
+ DASHED: 'dashed',
+ DOTTED: 'dotted',
+ SOLID: 'solid',
+};
diff --git a/src/components/shared/utils/constants/content_flags.ts b/src/components/shared/utils/constants/content_flags.ts
new file mode 100644
index 00000000..7d6efe6a
--- /dev/null
+++ b/src/components/shared/utils/constants/content_flags.ts
@@ -0,0 +1,8 @@
+export const ContentFlag = Object.freeze({
+ LOW_RISK_CR_EU: 'low_risk_cr_eu',
+ LOW_RISK_CR_NON_EU: 'low_risk_cr_non_eu',
+ HIGH_RISK_CR: 'high_risk_cr',
+ CR_DEMO: 'cr_demo',
+ EU_DEMO: 'eu_demo',
+ EU_REAL: 'eu_real',
+});
diff --git a/src/components/shared/utils/constants/contract.ts b/src/components/shared/utils/constants/contract.ts
new file mode 100644
index 00000000..0c8a6424
--- /dev/null
+++ b/src/components/shared/utils/constants/contract.ts
@@ -0,0 +1,627 @@
+import React from 'react';
+
+import { localize } from '@/utils/tmp/dummy';
+
+import { CONTRACT_TYPES, shouldShowCancellation, shouldShowExpiration, TRADE_TYPES } from '../contract';
+import { TContractOptions } from '../contract/contract-types';
+import { cloneObject } from '../object';
+import { LocalStore } from '../storage';
+
+export const getLocalizedBasis = () =>
+ ({
+ accumulator: localize('Accumulators'),
+ multiplier: localize('Multiplier'),
+ payout_per_pip: localize('Payout per pip'),
+ payout_per_point: localize('Payout per point'),
+ payout: localize('Payout'),
+ stake: localize('Stake'),
+ turbos: localize('Turbos'),
+ }) as const;
+
+/**
+ * components can be undef or an array containing any of: 'start_date', 'barrier', 'last_digit'
+ * ['duration', 'amount'] are omitted, as they're available in all contract types
+ */
+type TContractTypesConfig = {
+ title: string;
+ trade_types: string[];
+ basis: string[];
+ components: string[];
+ barrier_count?: number;
+ config?: { hide_duration?: boolean };
+};
+
+type TGetContractTypesConfig = (symbol?: string) => Record;
+
+type TContractConfig = {
+ button_name?: React.ReactNode;
+ feature_flag?: string;
+ name: React.ReactNode;
+ position: string;
+ main_title?: JSX.Element;
+};
+
+type TGetSupportedContracts = keyof ReturnType;
+
+export type TTextValueStrings = {
+ text: string;
+ value: string;
+};
+
+export type TTradeTypesCategories = {
+ [key: string]: {
+ name: string;
+ categories: Array;
+ };
+};
+
+export const getContractTypesConfig: TGetContractTypesConfig = symbol => ({
+ [TRADE_TYPES.RISE_FALL]: {
+ title: localize('Rise/Fall'),
+ trade_types: [CONTRACT_TYPES.CALL, CONTRACT_TYPES.PUT],
+ basis: ['stake', 'payout'],
+ components: ['start_date'],
+ barrier_count: 0,
+ },
+ [TRADE_TYPES.RISE_FALL_EQUAL]: {
+ title: localize('Rise/Fall'),
+ trade_types: [CONTRACT_TYPES.CALLE, CONTRACT_TYPES.PUTE],
+ basis: ['stake', 'payout'],
+ components: ['start_date'],
+ barrier_count: 0,
+ },
+ [TRADE_TYPES.HIGH_LOW]: {
+ title: localize('Higher/Lower'),
+ trade_types: [CONTRACT_TYPES.CALL, CONTRACT_TYPES.PUT],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ barrier_count: 1,
+ },
+ [TRADE_TYPES.TOUCH]: {
+ title: localize('Touch/No Touch'),
+ trade_types: [CONTRACT_TYPES.TOUCH.ONE_TOUCH, CONTRACT_TYPES.TOUCH.NO_TOUCH],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ },
+ [TRADE_TYPES.END]: {
+ title: localize('Ends In/Ends Out'),
+ trade_types: [CONTRACT_TYPES.END.IN, CONTRACT_TYPES.END.OUT],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ },
+ [TRADE_TYPES.STAY]: {
+ title: localize('Stays In/Goes Out'),
+ trade_types: [CONTRACT_TYPES.STAY.IN, CONTRACT_TYPES.STAY.OUT],
+ basis: ['stake', 'payout'],
+ components: ['barrier'],
+ },
+ [TRADE_TYPES.ASIAN]: {
+ title: localize('Asian Up/Asian Down'),
+ trade_types: [CONTRACT_TYPES.ASIAN.UP, CONTRACT_TYPES.ASIAN.DOWN],
+ basis: ['stake', 'payout'],
+ components: [],
+ },
+ [TRADE_TYPES.MATCH_DIFF]: {
+ title: localize('Matches/Differs'),
+ trade_types: [CONTRACT_TYPES.MATCH_DIFF.MATCH, CONTRACT_TYPES.MATCH_DIFF.DIFF],
+ basis: ['stake', 'payout'],
+ components: ['last_digit'],
+ },
+ [TRADE_TYPES.EVEN_ODD]: {
+ title: localize('Even/Odd'),
+ trade_types: [CONTRACT_TYPES.EVEN_ODD.ODD, CONTRACT_TYPES.EVEN_ODD.EVEN],
+ basis: ['stake', 'payout'],
+ components: [],
+ },
+ [TRADE_TYPES.OVER_UNDER]: {
+ title: localize('Over/Under'),
+ trade_types: [CONTRACT_TYPES.OVER_UNDER.OVER, CONTRACT_TYPES.OVER_UNDER.UNDER],
+ basis: ['stake', 'payout'],
+ components: ['last_digit'],
+ },
+ [TRADE_TYPES.LB_CALL]: {
+ title: localize('Close-to-Low'),
+ trade_types: [CONTRACT_TYPES.LB_CALL],
+ basis: ['multiplier'],
+ components: [],
+ },
+ [TRADE_TYPES.LB_PUT]: {
+ title: localize('High-to-Close'),
+ trade_types: [CONTRACT_TYPES.LB_PUT],
+ basis: ['multiplier'],
+ components: [],
+ },
+ [TRADE_TYPES.LB_HIGH_LOW]: {
+ title: localize('High-to-Low'),
+ trade_types: [CONTRACT_TYPES.LB_HIGH_LOW],
+ basis: ['multiplier'],
+ components: [],
+ },
+ [TRADE_TYPES.TICK_HIGH_LOW]: {
+ title: localize('High Tick/Low Tick'),
+ trade_types: [CONTRACT_TYPES.TICK_HIGH_LOW.HIGH, CONTRACT_TYPES.TICK_HIGH_LOW.LOW],
+ basis: [],
+ components: [],
+ },
+ [TRADE_TYPES.RUN_HIGH_LOW]: {
+ title: localize('Only Ups/Only Downs'),
+ trade_types: [CONTRACT_TYPES.RUN_HIGH_LOW.HIGH, CONTRACT_TYPES.RUN_HIGH_LOW.LOW],
+ basis: [],
+ components: [],
+ },
+ [TRADE_TYPES.RESET]: {
+ title: localize('Reset Up/Reset Down'),
+ trade_types: [CONTRACT_TYPES.RESET.CALL, CONTRACT_TYPES.RESET.PUT],
+ basis: [],
+ components: [],
+ },
+ [TRADE_TYPES.CALL_PUT_SPREAD]: {
+ title: localize('Spread Up/Spread Down'),
+ trade_types: [CONTRACT_TYPES.CALL_PUT_SPREAD.CALL, CONTRACT_TYPES.CALL_PUT_SPREAD.PUT],
+ basis: [],
+ components: [],
+ },
+ [TRADE_TYPES.ACCUMULATOR]: {
+ title: localize('Accumulators'),
+ trade_types: [CONTRACT_TYPES.ACCUMULATOR],
+ basis: ['stake'],
+ components: ['take_profit', 'accumulator', 'accu_info_display'],
+ barrier_count: 2,
+ config: { hide_duration: true },
+ },
+ [TRADE_TYPES.MULTIPLIER]: {
+ title: localize('Multipliers'),
+ trade_types: [CONTRACT_TYPES.MULTIPLIER.UP, CONTRACT_TYPES.MULTIPLIER.DOWN],
+ basis: ['stake'],
+ components: [
+ 'take_profit',
+ 'stop_loss',
+ ...(shouldShowCancellation(symbol) ? ['cancellation'] : []),
+ ...(shouldShowExpiration(symbol) ? ['expiration'] : []),
+ ],
+ config: { hide_duration: true },
+ }, // hide Duration for Multiplier contracts for now
+ [TRADE_TYPES.TURBOS.LONG]: {
+ title: localize('Turbos'),
+ trade_types: [CONTRACT_TYPES.TURBOS.LONG],
+ basis: ['stake'],
+ barrier_count: 1,
+ components: ['trade_type_tabs', 'barrier_selector', 'take_profit'],
+ },
+ [TRADE_TYPES.TURBOS.SHORT]: {
+ title: localize('Turbos'),
+ trade_types: [CONTRACT_TYPES.TURBOS.SHORT],
+ basis: ['stake'],
+ barrier_count: 1,
+ components: ['trade_type_tabs', 'barrier_selector', 'take_profit'],
+ },
+ [TRADE_TYPES.VANILLA.CALL]: {
+ title: localize('Call/Put'),
+ trade_types: [CONTRACT_TYPES.VANILLA.CALL],
+ basis: ['stake'],
+ components: ['duration', 'strike', 'amount', 'trade_type_tabs'],
+ barrier_count: 1,
+ },
+ [TRADE_TYPES.VANILLA.PUT]: {
+ title: localize('Call/Put'),
+ trade_types: [CONTRACT_TYPES.VANILLA.PUT],
+ basis: ['stake'],
+ components: ['duration', 'strike', 'amount', 'trade_type_tabs'],
+ barrier_count: 1,
+ },
+});
+
+// Config for rendering trade options
+export const getContractCategoriesConfig = () =>
+ ({
+ Turbos: { name: localize('Turbos'), categories: [TRADE_TYPES.TURBOS.LONG, TRADE_TYPES.TURBOS.SHORT] },
+ Multipliers: { name: localize('Multipliers'), categories: [TRADE_TYPES.MULTIPLIER] },
+ 'Ups & Downs': {
+ name: localize('Ups & Downs'),
+ categories: [
+ TRADE_TYPES.RISE_FALL,
+ TRADE_TYPES.RISE_FALL_EQUAL,
+ TRADE_TYPES.HIGH_LOW,
+ TRADE_TYPES.RUN_HIGH_LOW,
+ TRADE_TYPES.RESET,
+ TRADE_TYPES.ASIAN,
+ TRADE_TYPES.CALL_PUT_SPREAD,
+ ],
+ },
+ 'Highs & Lows': {
+ name: localize('Highs & Lows'),
+ categories: [TRADE_TYPES.TOUCH, TRADE_TYPES.TICK_HIGH_LOW],
+ },
+ 'Ins & Outs': { name: localize('Ins & Outs'), categories: [TRADE_TYPES.END, TRADE_TYPES.STAY] },
+ 'Look Backs': {
+ name: localize('Look Backs'),
+ categories: [TRADE_TYPES.LB_HIGH_LOW, TRADE_TYPES.LB_PUT, TRADE_TYPES.LB_CALL],
+ },
+ Digits: {
+ name: localize('Digits'),
+ categories: [TRADE_TYPES.MATCH_DIFF, TRADE_TYPES.EVEN_ODD, TRADE_TYPES.OVER_UNDER],
+ },
+ Vanillas: { name: localize('Vanillas'), categories: [TRADE_TYPES.VANILLA.CALL, TRADE_TYPES.VANILLA.PUT] },
+ Accumulators: { name: localize('Accumulators'), categories: [TRADE_TYPES.ACCUMULATOR] },
+ }) as const;
+
+export const unsupported_contract_types_list = [
+ // TODO: remove these once all contract types are supported
+ TRADE_TYPES.CALL_PUT_SPREAD,
+ TRADE_TYPES.RUN_HIGH_LOW,
+ TRADE_TYPES.RESET,
+ TRADE_TYPES.ASIAN,
+ TRADE_TYPES.TICK_HIGH_LOW,
+ TRADE_TYPES.END,
+ TRADE_TYPES.STAY,
+ TRADE_TYPES.LB_CALL,
+ TRADE_TYPES.LB_PUT,
+ TRADE_TYPES.LB_HIGH_LOW,
+] as const;
+
+export const getCardLabels = () =>
+ ({
+ APPLY: localize('Apply'),
+ BARRIER: localize('Barrier:'),
+ BUY_PRICE: localize('Buy price:'),
+ CANCEL: localize('Cancel'),
+ CLOSE: localize('Close'),
+ CLOSED: localize('Closed'),
+ CONTRACT_COST: localize('Contract cost:'),
+ CONTRACT_VALUE: localize('Contract value:'),
+ CURRENT_STAKE: localize('Current stake:'),
+ DAY: localize('day'),
+ DAYS: localize('days'),
+ DEAL_CANCEL_FEE: localize('Deal cancel. fee:'),
+ DECREMENT_VALUE: localize('Decrement value'),
+ DONT_SHOW_THIS_AGAIN: localize("Don't show this again"),
+ ENTRY_SPOT: localize('Entry spot:'),
+ INCREMENT_VALUE: localize('Increment value'),
+ INDICATIVE_PRICE: localize('Indicative price:'),
+ INITIAL_STAKE: localize('Initial stake:'),
+ LOST: localize('Lost'),
+ MULTIPLIER: localize('Multiplier:'),
+ NOT_AVAILABLE: localize('N/A'),
+ PAYOUT: localize('Sell price:'),
+ POTENTIAL_PAYOUT: localize('Potential payout:'),
+ POTENTIAL_PROFIT_LOSS: localize('Potential profit/loss:'),
+ PROFIT_LOSS: localize('Profit/Loss:'),
+ PURCHASE_PRICE: localize('Buy price:'),
+ RESALE_NOT_OFFERED: localize('Resale not offered'),
+ SELL: localize('Sell'),
+ STAKE: localize('Stake:'),
+ STOP_LOSS: localize('Stop loss:'),
+ STRIKE: localize('Strike:'),
+ TAKE_PROFIT: localize('Take profit:'),
+ TICK: localize('Tick '),
+ TICKS: localize('Ticks'),
+ TOTAL_PROFIT_LOSS: localize('Total profit/loss:'),
+ TAKE_PROFIT_LOSS_NOT_AVAILABLE: localize(
+ 'Take profit and/or stop loss are not available while deal cancellation is active.'
+ ),
+ WON: localize('Won'),
+ }) as const;
+
+export const getMarketNamesMap = () =>
+ ({
+ FRXAUDCAD: localize('AUD/CAD'),
+ FRXAUDCHF: localize('AUD/CHF'),
+ FRXAUDJPY: localize('AUD/JPY'),
+ FRXAUDNZD: localize('AUD/NZD'),
+ FRXAUDPLN: localize('AUD/PLN'),
+ FRXAUDUSD: localize('AUD/USD'),
+ FRXBROUSD: localize('Oil/USD'),
+ FRXEURAUD: localize('EUR/AUD'),
+ FRXEURCAD: localize('EUR/CAD'),
+ FRXEURCHF: localize('EUR/CHF'),
+ FRXEURGBP: localize('EUR/GBP'),
+ FRXEURJPY: localize('EUR/JPY'),
+ FRXEURNZD: localize('EUR/NZD'),
+ FRXEURUSD: localize('EUR/USD'),
+ FRXGBPAUD: localize('GBP/AUD'),
+ FRXGBPCAD: localize('GBP/CAD'),
+ FRXGBPCHF: localize('GBP/CHF'),
+ FRXGBPJPY: localize('GBP/JPY'),
+ FRXGBPNOK: localize('GBP/NOK'),
+ FRXGBPUSD: localize('GBP/USD'),
+ FRXNZDJPY: localize('NZD/JPY'),
+ FRXNZDUSD: localize('NZD/USD'),
+ FRXUSDCAD: localize('USD/CAD'),
+ FRXUSDCHF: localize('USD/CHF'),
+ FRXUSDJPY: localize('USD/JPY'),
+ FRXUSDNOK: localize('USD/NOK'),
+ FRXUSDPLN: localize('USD/PLN'),
+ FRXUSDSEK: localize('USD/SEK'),
+ FRXXAGUSD: localize('Silver/USD'),
+ FRXXAUUSD: localize('Gold/USD'),
+ FRXXPDUSD: localize('Palladium/USD'),
+ FRXXPTUSD: localize('Platinum/USD'),
+ OTC_AEX: localize('Netherlands 25'),
+ OTC_AS51: localize('Australia 200'),
+ OTC_DJI: localize('Wall Street 30'),
+ OTC_FCHI: localize('France 40'),
+ OTC_FTSE: localize('UK 100'),
+ OTC_GDAXI: localize('Germany 40'),
+ OTC_HSI: localize('Hong Kong 50'),
+ OTC_IBEX35: localize('Spanish Index'),
+ OTC_N225: localize('Japan 225'),
+ OTC_NDX: localize('US Tech 100'),
+ OTC_SPC: localize('US 500'),
+ OTC_SSMI: localize('Swiss 20'),
+ OTC_SX5E: localize('Euro 50'),
+ R_10: localize('Volatility 10 Index'),
+ R_25: localize('Volatility 25 Index'),
+ R_50: localize('Volatility 50 Index'),
+ R_75: localize('Volatility 75 Index'),
+ R_100: localize('Volatility 100 Index'),
+ BOOM300N: localize('Boom 300 Index'),
+ BOOM500: localize('Boom 500 Index'),
+ BOOM1000: localize('Boom 1000 Index'),
+ CRASH300N: localize('Crash 300 Index'),
+ CRASH500: localize('Crash 500 Index'),
+ CRASH1000: localize('Crash 1000 Index'),
+ RDBEAR: localize('Bear Market Index'),
+ RDBULL: localize('Bull Market Index'),
+ STPRNG: localize('Step Index'),
+ WLDAUD: localize('AUD Basket'),
+ WLDEUR: localize('EUR Basket'),
+ WLDGBP: localize('GBP Basket'),
+ WLDXAU: localize('Gold Basket'),
+ WLDUSD: localize('USD Basket'),
+ '1HZ10V': localize('Volatility 10 (1s) Index'),
+ '1HZ25V': localize('Volatility 25 (1s) Index'),
+ '1HZ50V': localize('Volatility 50 (1s) Index'),
+ '1HZ75V': localize('Volatility 75 (1s) Index'),
+ '1HZ100V': localize('Volatility 100 (1s) Index'),
+ '1HZ150V': localize('Volatility 150 (1s) Index'),
+ '1HZ200V': localize('Volatility 200 (1s) Index'),
+ '1HZ250V': localize('Volatility 250 (1s) Index'),
+ '1HZ300V': localize('Volatility 300 (1s) Index'),
+ JD10: localize('Jump 10 Index'),
+ JD25: localize('Jump 25 Index'),
+ JD50: localize('Jump 50 Index'),
+ JD75: localize('Jump 75 Index'),
+ JD100: localize('Jump 100 Index'),
+ JD150: localize('Jump 150 Index'),
+ JD200: localize('Jump 200 Index'),
+ CRYBCHUSD: localize('BCH/USD'),
+ CRYBNBUSD: localize('BNB/USD'),
+ CRYBTCLTC: localize('BTC/LTC'),
+ CRYIOTUSD: localize('IOT/USD'),
+ CRYNEOUSD: localize('NEO/USD'),
+ CRYOMGUSD: localize('OMG/USD'),
+ CRYTRXUSD: localize('TRX/USD'),
+ CRYBTCETH: localize('BTC/ETH'),
+ CRYZECUSD: localize('ZEC/USD'),
+ CRYXMRUSD: localize('ZMR/USD'),
+ CRYXMLUSD: localize('XLM/USD'),
+ CRYXRPUSD: localize('XRP/USD'),
+ CRYBTCUSD: localize('BTC/USD'),
+ CRYDSHUSD: localize('DSH/USD'),
+ CRYETHUSD: localize('ETH/USD'),
+ CRYEOSUSD: localize('EOS/USD'),
+ CRYLTCUSD: localize('LTC/USD'),
+ }) as const;
+
+export const getUnsupportedContracts = () =>
+ ({
+ CALLSPREAD: {
+ name: localize('Spread Up'),
+ position: 'top',
+ },
+ PUTSPREAD: {
+ name: localize('Spread Down'),
+ position: 'bottom',
+ },
+ }) as const;
+
+/**
+ * // Config to display details such as trade buttons, their positions, and names of trade types
+ *
+ * @param {Boolean} is_high_low
+ * @returns { object }
+ */
+export const getSupportedContracts = (is_high_low?: boolean) =>
+ ({
+ [CONTRACT_TYPES.ACCUMULATOR]: {
+ button_name: localize('Buy'),
+ name: localize('Accumulators'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.CALL]: {
+ name: is_high_low ? localize('Higher') : localize('Rise'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.PUT]: {
+ name: is_high_low ? localize('Lower') : localize('Fall'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.CALLE]: {
+ name: localize('Rise'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.PUTE]: {
+ name: localize('Fall'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.MATCH_DIFF.MATCH]: {
+ name: localize('Matches'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.MATCH_DIFF.DIFF]: {
+ name: localize('Differs'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.EVEN_ODD.EVEN]: {
+ name: localize('Even'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.EVEN_ODD.ODD]: {
+ name: localize('Odd'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.OVER_UNDER.OVER]: {
+ name: localize('Over'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.OVER_UNDER.UNDER]: {
+ name: localize('Under'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.TOUCH.ONE_TOUCH]: {
+ name: localize('Touch'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.TOUCH.NO_TOUCH]: {
+ name: localize('No Touch'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.MULTIPLIER.UP]: {
+ name: localize('Up'),
+ position: 'top',
+ main_title: localize('Multipliers'),
+ },
+ [CONTRACT_TYPES.MULTIPLIER.DOWN]: {
+ name: localize('Down'),
+ position: 'bottom',
+ main_title: localize('Multipliers'),
+ },
+ [CONTRACT_TYPES.TURBOS.LONG]: {
+ name: localize('Up'),
+ position: 'top',
+ main_title: localize('Turbos'),
+ },
+ [CONTRACT_TYPES.TURBOS.SHORT]: {
+ name: localize('Down'),
+ position: 'bottom',
+ main_title: localize('Turbos'),
+ },
+ [CONTRACT_TYPES.VANILLA.CALL]: {
+ name: localize('Call'),
+ position: 'top',
+ main_title: localize('Vanillas'),
+ },
+ [CONTRACT_TYPES.VANILLA.PUT]: {
+ name: localize('Put'),
+ position: 'bottom',
+ main_title: localize('Vanillas'),
+ },
+ [CONTRACT_TYPES.RUN_HIGH_LOW.HIGH]: {
+ name: localize('Only Ups'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.RUN_HIGH_LOW.LOW]: {
+ name: localize('Only Downs'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.END.OUT]: {
+ name: localize('Ends Outside'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.END.IN]: {
+ name: localize('Ends Between'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.STAY.IN]: {
+ name: localize('Stays Between'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.STAY.OUT]: {
+ name: localize('Goes Outside'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.ASIAN.UP]: {
+ name: localize('Asian Up'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.ASIAN.DOWN]: {
+ name: localize('Asian Down'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.TICK_HIGH_LOW.HIGH]: {
+ name: localize('High Tick'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.TICK_HIGH_LOW.LOW]: {
+ name: localize('Low Tick'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.RESET.CALL]: {
+ name: localize('Reset Call'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.RESET.PUT]: {
+ name: localize('Reset Put'),
+ position: 'bottom',
+ },
+ [CONTRACT_TYPES.LB_CALL]: {
+ name: localize('Close-Low'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.LB_PUT]: {
+ name: localize('High-Close'),
+ position: 'top',
+ },
+ [CONTRACT_TYPES.LB_HIGH_LOW]: {
+ name: localize('High-Low'),
+ position: 'top',
+ },
+ // To add a feature flag for a new trade_type, please add 'feature_flag' to its config here:
+ // SHARKFIN: {
+ // feature_flag: 'sharkfin',
+ // name: localize('Sharkfin'),
+ // position: 'top',
+ // }
+ // and also to DTRADER_FLAGS in FeatureFlagsStore, e.g.: sharkfin: false,
+ }) as const;
+
+export const TRADE_FEATURE_FLAGS = ['sharkfin', 'dtrader_v2'];
+
+export const getCleanedUpCategories = (categories: TTradeTypesCategories) => {
+ const categories_copy: TTradeTypesCategories = cloneObject(categories);
+ const hidden_trade_types = Object.entries(LocalStore.getObject('FeatureFlagsStore')?.data ?? {})
+ .filter(([key, value]) => TRADE_FEATURE_FLAGS.includes(key) && !value)
+ .map(([key]) => key);
+
+ return Object.keys(categories_copy).reduce((acc, key) => {
+ const category = categories_copy[key].categories?.filter(item => {
+ return (
+ typeof item === 'object' &&
+ // hide trade types with disabled feature flag:
+ hidden_trade_types?.every(hidden_type => !item.value.startsWith(hidden_type))
+ );
+ });
+ if (category?.length === 0) {
+ delete acc[key];
+ } else {
+ acc[key].categories = category;
+ }
+ return acc;
+ }, categories_copy);
+};
+
+export const getContractConfig = (is_high_low?: boolean) => ({
+ ...getSupportedContracts(is_high_low),
+ ...getUnsupportedContracts(),
+});
+
+export const getContractTypeDisplay = (type: string, options: TContractOptions = {}) => {
+ const { isHighLow = false, showButtonName = false, showMainTitle = false } = options;
+
+ const contract_config = getContractConfig(isHighLow)[type as TGetSupportedContracts] as TContractConfig;
+ if (showMainTitle) return contract_config?.main_title ?? '';
+ return (showButtonName && contract_config?.button_name) || contract_config?.name || '';
+};
+
+export const getContractTypeFeatureFlag = (type: string, is_high_low = false) => {
+ const contract_config = getContractConfig(is_high_low)[type as TGetSupportedContracts] as TContractConfig;
+ return contract_config?.feature_flag ?? '';
+};
+
+export const getContractTypePosition = (type: TGetSupportedContracts, is_high_low = false) =>
+ getContractConfig(is_high_low)?.[type]?.position || 'top';
+
+export const isCallPut = (trade_type: 'rise_fall' | 'rise_fall_equal' | 'high_low'): boolean =>
+ trade_type === TRADE_TYPES.RISE_FALL ||
+ trade_type === TRADE_TYPES.RISE_FALL_EQUAL ||
+ trade_type === TRADE_TYPES.HIGH_LOW;
diff --git a/src/components/shared/utils/constants/default-options.ts b/src/components/shared/utils/constants/default-options.ts
new file mode 100644
index 00000000..14b5efb1
--- /dev/null
+++ b/src/components/shared/utils/constants/default-options.ts
@@ -0,0 +1,18 @@
+import { localize } from '@/utils/tmp/dummy';
+
+/**
+ * Returns an object that allows user to skip IDV
+ */
+
+export const getIDVNotApplicableOption = (is_for_real_account_signup_modal?: boolean) => ({
+ id: 'none',
+ text: is_for_real_account_signup_modal
+ ? localize('I want to do this later')
+ : localize("I don't have any of these"),
+ value: 'none',
+});
+
+/**
+ * Returns default value for the text to render when there are no matching results.
+ */
+export const getSearchNotFoundOption = () => localize('No results found');
diff --git a/src/components/shared/utils/constants/error.ts b/src/components/shared/utils/constants/error.ts
new file mode 100644
index 00000000..fab38910
--- /dev/null
+++ b/src/components/shared/utils/constants/error.ts
@@ -0,0 +1,16 @@
+import { localize } from '@/utils/tmp/dummy';
+
+export const getDefaultError = () => ({
+ header: localize('Sorry for the interruption'),
+ description: localize('Our servers hit a bump. Let’s refresh to move on.'),
+ cta_label: localize('Refresh'),
+});
+
+export const STATUS_CODES = Object.freeze({
+ NONE: 'none',
+ PENDING: 'pending',
+ REJECTED: 'rejected',
+ VERIFIED: 'verified',
+ EXPIRED: 'expired',
+ SUSPECTED: 'suspected',
+});
diff --git a/src/components/shared/utils/constants/index.ts b/src/components/shared/utils/constants/index.ts
new file mode 100644
index 00000000..d1ec0ed4
--- /dev/null
+++ b/src/components/shared/utils/constants/index.ts
@@ -0,0 +1,10 @@
+export * from './barriers';
+export * from './contract';
+export * from './content_flags';
+export * from './default-options';
+export * from './jurisdictions-config';
+export * from './signup_fields';
+export * from './error';
+export * from './poi-failure-codes';
+export * from './mt5-login-list-status';
+export * from './auth-status-codes';
diff --git a/src/components/shared/utils/constants/jurisdictions-config.ts b/src/components/shared/utils/constants/jurisdictions-config.ts
new file mode 100644
index 00000000..fea8c3ba
--- /dev/null
+++ b/src/components/shared/utils/constants/jurisdictions-config.ts
@@ -0,0 +1,27 @@
+export const Jurisdiction = Object.freeze({
+ SVG: 'svg',
+ BVI: 'bvi',
+ VANUATU: 'vanuatu',
+ LABUAN: 'labuan',
+ MALTA_INVEST: 'maltainvest',
+});
+
+export const Platforms = Object.freeze({
+ DXTRADE: 'dxtrade',
+ MT5: 'mt5',
+});
+
+export const DBVI_COMPANY_NAMES = {
+ bvi: { name: 'Deriv (BVI) Ltd', tnc_url: 'tnc/deriv-(bvi)-ltd.pdf' },
+ labuan: { name: 'Deriv (FX) Ltd', tnc_url: 'tnc/deriv-(fx)-ltd.pdf' },
+ maltainvest: {
+ name: 'Deriv Investments (Europe) Limited',
+ tnc_url: 'tnc/deriv-investments-(europe)-limited.pdf',
+ },
+ vanuatu: { name: 'Deriv (V) Ltd', tnc_url: 'tnc/general-terms.pdf' },
+} as const;
+
+export const JURISDICTION_MARKET_TYPES = {
+ FINANCIAL: 'financial',
+ DERIVED: 'derived',
+} as const;
diff --git a/src/components/shared/utils/constants/mt5-login-list-status.ts b/src/components/shared/utils/constants/mt5-login-list-status.ts
new file mode 100644
index 00000000..de40b430
--- /dev/null
+++ b/src/components/shared/utils/constants/mt5-login-list-status.ts
@@ -0,0 +1,9 @@
+export const MT5_ACCOUNT_STATUS = Object.freeze({
+ FAILED: 'failed',
+ MIGRATED_WITH_POSITION: 'migrated_with_position',
+ MIGRATED_WITHOUT_POSITION: 'migrated_without_position',
+ NEEDS_VERIFICATION: 'needs_verification',
+ PENDING: 'pending',
+ POA_PENDING: 'poa_pending',
+ POA_VERIFIED: 'poa_verified',
+});
diff --git a/src/components/shared/utils/constants/poi-failure-codes.tsx b/src/components/shared/utils/constants/poi-failure-codes.tsx
new file mode 100644
index 00000000..446ad03f
--- /dev/null
+++ b/src/components/shared/utils/constants/poi-failure-codes.tsx
@@ -0,0 +1,356 @@
+import React from 'react';
+
+import { Localize } from '@/utils/tmp/dummy';
+
+type TIDVErrorStatus = Readonly<{
+ [key: string]: {
+ code: keyof typeof IDV_ERROR_STATUS;
+ message: React.ReactNode;
+ };
+}>;
+
+type TOnfidoErrorStatus = Readonly<{
+ [key: string]: {
+ code: keyof typeof ONFIDO_ERROR_STATUS;
+ message: React.ReactNode;
+ };
+}>;
+
+export const IDV_ERROR_STATUS: TIDVErrorStatus = Object.freeze({
+ DobMismatch: {
+ code: 'DobMismatch',
+ message: (
+ ]}
+ />
+ ),
+ },
+ DocumentRejected: {
+ code: 'DocumentRejected',
+ message: (
+
+ ),
+ },
+ EmptyStatus: {
+ code: 'EmptyStatus',
+ message: ,
+ },
+ Expired: { code: 'Expired', message: },
+ InformationLack: {
+ code: 'InformationLack',
+ message: (
+
+ ),
+ },
+ MalformedJson: {
+ code: 'MalformedJson',
+ message: (
+
+ ),
+ },
+ NameMismatch: {
+ code: 'NameMismatch',
+ message: (
+ ]}
+ />
+ ),
+ },
+ RejectedByProvider: {
+ code: 'RejectedByProvider',
+ message: ,
+ },
+ Underage: {
+ code: 'Underage',
+ message: ,
+ },
+ Deceased: {
+ code: 'Deceased',
+ message: ,
+ },
+ Failed: {
+ code: 'Failed',
+ message: (
+
+ ),
+ },
+ NameDobMismatch: {
+ code: 'NameDobMismatch',
+ message: (
+ ]}
+ />
+ ),
+ },
+ NeedsTechnicalInvestigation: {
+ code: 'NeedsTechnicalInvestigation',
+ message: (
+
+ ),
+ },
+ HighRisk: {
+ code: 'HighRisk',
+ message: (
+
+ ),
+ },
+ ReportNotAvailable: {
+ code: 'ReportNotAvailable',
+ message: (
+
+ ),
+ },
+});
+
+export const ONFIDO_ERROR_STATUS: TOnfidoErrorStatus = Object.freeze({
+ AgeValidationMinimumAcceptedAge: {
+ code: 'AgeValidationMinimumAcceptedAge',
+ message: (
+
+ ),
+ },
+ CompromisedDocument: {
+ code: 'CompromisedDocument',
+ message: ,
+ },
+ DataComparisonDateOfBirth: {
+ code: 'DataComparisonDateOfBirth',
+ message: ,
+ },
+ DataComparisonDateOfExpiry: {
+ code: 'DataComparisonDateOfExpiry',
+ message: ,
+ },
+ DataComparisonDocumentNumbers: {
+ code: 'DataComparisonDocumentNumbers',
+ message: ,
+ },
+ DataComparisonDocumentType: {
+ code: 'DataComparisonDocumentType',
+ message: ,
+ },
+ DataComparisonIssuingCountry: {
+ code: 'DataComparisonIssuingCountry',
+ message: ,
+ },
+ DataComparisonName: {
+ code: 'DataComparisonName',
+ message: ,
+ },
+ DataValidationDateOfBirth: {
+ code: 'DataValidationDateOfBirth',
+ message: (
+
+ ),
+ },
+ DataValidationDocumentExpiration: {
+ code: 'DataValidationDocumentExpiration',
+ message: ,
+ },
+ DataValidationDocumentNumbers: {
+ code: 'DataValidationDocumentNumbers',
+ message: (
+
+ ),
+ },
+ DataValidationExpiryDate: {
+ code: 'DataValidationExpiryDate',
+ message: (
+
+ ),
+ },
+ DataValidationMrz: {
+ code: 'DataValidationMrz',
+ message: (
+
+ ),
+ },
+ DataValidationNoDocumentNumbers: {
+ code: 'DataValidationNoDocumentNumbers',
+ message: ,
+ },
+ DuplicatedDocument: {
+ code: 'DuplicatedDocument',
+ message: ,
+ },
+ Expired: { code: 'Expired', message: },
+ ImageIntegrityColourPicture: {
+ code: 'ImageIntegrityColourPicture',
+ message: (
+
+ ),
+ },
+ ImageIntegrityConclusiveDocumentQuality: {
+ code: 'ImageIntegrityConclusiveDocumentQuality',
+ message: ,
+ },
+ ImageIntegrityConclusiveDocumentQualityAbnormalDocumentFeatures: {
+ code: 'ImageIntegrityConclusiveDocumentQualityAbnormalDocumentFeatures',
+ message: (
+
+ ),
+ },
+ ImageIntegrityConclusiveDocumentQualityCornerRemoved: {
+ code: 'ImageIntegrityConclusiveDocumentQualityCornerRemoved',
+ message: ,
+ },
+ ImageIntegrityConclusiveDocumentQualityDigitalDocument: {
+ code: 'ImageIntegrityConclusiveDocumentQualityDigitalDocument',
+ message: ,
+ },
+ ImageIntegrityConclusiveDocumentQualityMissingBack: {
+ code: 'ImageIntegrityConclusiveDocumentQualityMissingBack',
+ message: (
+
+ ),
+ },
+ ImageIntegrityConclusiveDocumentQualityObscuredDataPoints: {
+ code: 'ImageIntegrityConclusiveDocumentQualityObscuredDataPoints',
+ message: (
+
+ ),
+ },
+ ImageIntegrityConclusiveDocumentQualityObscuredSecurityFeatures: {
+ code: 'ImageIntegrityConclusiveDocumentQualityObscuredSecurityFeatures',
+ message: (
+
+ ),
+ },
+ ImageIntegrityConclusiveDocumentQualityPuncturedDocument: {
+ code: 'ImageIntegrityConclusiveDocumentQualityPuncturedDocument',
+ message: ,
+ },
+ ImageIntegrityConclusiveDocumentQualityWatermarksDigitalTextOverlay: {
+ code: 'ImageIntegrityConclusiveDocumentQualityWatermarksDigitalTextOverlay',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQuality: {
+ code: 'ImageIntegrityImageQuality',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityBlurredPhoto: {
+ code: 'ImageIntegrityImageQualityBlurredPhoto',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityCoveredPhoto: {
+ code: 'ImageIntegrityImageQualityCoveredPhoto',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityCutOffDocument: {
+ code: 'ImageIntegrityImageQualityCutOffDocument',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityDamagedDocument: {
+ code: 'ImageIntegrityImageQualityDamagedDocument',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityDarkPhoto: {
+ code: 'ImageIntegrityImageQualityDarkPhoto',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityGlareOnPhoto: {
+ code: 'ImageIntegrityImageQualityGlareOnPhoto',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityIncorrectSide: {
+ code: 'ImageIntegrityImageQualityIncorrectSide',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityNoDocumentInImage: {
+ code: 'ImageIntegrityImageQualityNoDocumentInImage',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityOtherPhotoIssue: {
+ code: 'ImageIntegrityImageQualityOtherPhotoIssue',
+ message: (
+
+ ),
+ },
+ ImageIntegrityImageQualityTwoDocumentsUploaded: {
+ code: 'ImageIntegrityImageQualityTwoDocumentsUploaded',
+ message: (
+
+ ),
+ },
+ ImageIntegritySupportedDocument: {
+ code: 'ImageIntegritySupportedDocument',
+ message: (
+
+ ),
+ },
+ SelfieRejected: {
+ code: 'SelfieRejected',
+ message: ,
+ },
+ VisualAuthenticityDigitalTampering: {
+ code: 'VisualAuthenticityDigitalTampering',
+ message: ,
+ },
+ VisualAuthenticityFaceDetection: {
+ code: 'VisualAuthenticityFaceDetection',
+ message: ,
+ },
+ VisualAuthenticityFonts: {
+ code: 'VisualAuthenticityFonts',
+ message: ,
+ },
+ VisualAuthenticityOriginalDocumentPresent: {
+ code: 'VisualAuthenticityOriginalDocumentPresent',
+ message: (
+
+ ),
+ },
+ VisualAuthenticityOriginalDocumentPresentDocumentOnPrintedPaper: {
+ code: 'VisualAuthenticityOriginalDocumentPresentDocumentOnPrintedPaper',
+ message: ,
+ },
+ VisualAuthenticityOriginalDocumentPresentPhotoOfScreen: {
+ code: 'VisualAuthenticityOriginalDocumentPresentPhotoOfScreen',
+ message: ,
+ },
+ VisualAuthenticityOriginalDocumentPresentScan: {
+ code: 'VisualAuthenticityOriginalDocumentPresentScan',
+ message: (
+
+ ),
+ },
+ VisualAuthenticityOriginalDocumentPresentScreenshot: {
+ code: 'VisualAuthenticityOriginalDocumentPresentScreenshot',
+ message: ,
+ },
+ VisualAuthenticityPictureFaceIntegrity: {
+ code: 'VisualAuthenticityPictureFaceIntegrity',
+ message: ,
+ },
+ VisualAuthenticitySecurityFeatures: {
+ code: 'VisualAuthenticitySecurityFeatures',
+ message: ,
+ },
+ VisualAuthenticityTemplate: {
+ code: 'VisualAuthenticityTemplate',
+ message: ,
+ },
+});
diff --git a/src/components/shared/utils/constants/signup_fields.ts b/src/components/shared/utils/constants/signup_fields.ts
new file mode 100644
index 00000000..9a905861
--- /dev/null
+++ b/src/components/shared/utils/constants/signup_fields.ts
@@ -0,0 +1,32 @@
+import { localize } from '@/utils/tmp/dummy';
+
+export const getAddressDetailsFields = () => ({
+ address_line_1: localize('First line of address'),
+ address_line_2: localize('Second line of address'),
+ address_city: localize('Town/City'),
+ address_state: localize('State/Province'),
+ address_postcode: localize('Postal/ZIP code'),
+});
+
+export const getPersonalDetailsFields = () => ({
+ salutation: localize('Title and name'),
+ first_name: localize('First name'),
+ last_name: localize('Last name'),
+ date_of_birth: localize('Date of birth'),
+ place_of_birth: localize('Place of birth'),
+ phone: localize('Phone number'),
+ citizen: localize('Citizenship'),
+ residence: localize('Country of residence'),
+ tax_identification_number: localize('Tax identification number'),
+ tax_residence: localize('Tax residence'),
+ account_opening_reason: localize('Account opening reason'),
+ employment_status: localize('Employment status'),
+});
+
+export const getSignupFormFields = () => ({ ...getPersonalDetailsFields(), ...getAddressDetailsFields() });
+
+export const EMPLOYMENT_VALUES = Object.freeze({
+ EMPLOYED: 'Employed',
+ UNEMPLOYED: 'Unemployed',
+ SELF_EMPLOYED: 'Self-Employed',
+});
diff --git a/src/components/shared/utils/contract/contract-info.ts b/src/components/shared/utils/contract/contract-info.ts
new file mode 100644
index 00000000..6fe1e6ac
--- /dev/null
+++ b/src/components/shared/utils/contract/contract-info.ts
@@ -0,0 +1,45 @@
+import { TContractInfo } from './contract-types';
+
+export const mockContractInfo = (obj: Partial = {}): TContractInfo => {
+ return {
+ account_id: 84780920,
+ barrier_count: 1,
+ bid_price: 19.32,
+ buy_price: 10,
+ contract_id: 224304409908,
+ contract_type: 'CALL',
+ currency: 'USD',
+ current_spot: 2415.18,
+ current_spot_display_value: '2415.18',
+ current_spot_time: 1700481950,
+ date_expiry: 1700482235,
+ date_settlement: 1700482235,
+ date_start: 1700481935,
+ display_name: 'Volatility 100 (1s) Index',
+ expiry_time: 1700482235,
+ id: '1498e9ed-ef07-a793-76aa-4bee79cab438',
+ is_expired: 0,
+ is_forward_starting: 0,
+ is_intraday: 1,
+ is_path_dependent: 0,
+ is_settleable: 0,
+ is_sold: 0,
+ is_valid_to_cancel: 0,
+ is_valid_to_sell: 0,
+ longcode:
+ 'Win payout if Volatility 100 (1s) Index is strictly higher than entry spot at 5 minutes after contract start time.',
+ payout: 19.55,
+ profit: 9.32,
+ profit_percentage: 93.2,
+ purchase_time: 1700481935,
+ shortcode: 'CALL_1HZ100V_19.55_1700481935_1700482235_S0P_0',
+ status: 'open',
+ transaction_ids: {
+ buy: 447576512008,
+ },
+ underlying: '1HZ100V',
+ validation_error: 'Waiting for entry tick.',
+ validation_error_code: 'General',
+ ...obj,
+ };
+};
diff --git a/src/components/shared/utils/contract/contract-types.ts b/src/components/shared/utils/contract/contract-types.ts
new file mode 100644
index 00000000..49b6b80a
--- /dev/null
+++ b/src/components/shared/utils/contract/contract-types.ts
@@ -0,0 +1,46 @@
+import { ContractUpdate, ContractUpdateHistory, Portfolio1, ProposalOpenContract } from '@deriv/api-types';
+
+export type TContractStore = {
+ clearContractUpdateConfigValues: () => void;
+ contract_info: TContractInfo;
+ contract_update_history: ContractUpdateHistory;
+ contract_update_take_profit: number | string;
+ contract_update_stop_loss: number | string;
+ digits_info: TDigitsInfo;
+ display_status: string;
+ has_contract_update_take_profit: boolean;
+ has_contract_update_stop_loss: boolean;
+ is_digit_contract: boolean;
+ is_ended: boolean;
+ onChange: (param: { name: string; value: string | number | boolean }) => void;
+ updateLimitOrder: () => void;
+ validation_errors: { contract_update_stop_loss: string[]; contract_update_take_profit: string[] };
+};
+
+export type TContractInfo = ProposalOpenContract &
+ Portfolio1 & {
+ contract_update?: ContractUpdate;
+ };
+
+export type TTickItem = {
+ epoch?: number;
+ tick?: null | number;
+ tick_display_value?: null | string;
+};
+
+export type TDigitsInfo = { [key: number]: { digit: number; spot: string } };
+
+type TLimitProperty = {
+ display_name?: string;
+ order_amount?: null | number;
+ order_date?: number;
+ value?: null | string;
+};
+
+export type TLimitOrder = Partial>;
+
+export type TContractOptions = {
+ isHighLow?: boolean;
+ showButtonName?: boolean;
+ showMainTitle?: boolean;
+};
diff --git a/src/components/shared/utils/contract/contract.tsx b/src/components/shared/utils/contract/contract.tsx
new file mode 100644
index 00000000..adef6f22
--- /dev/null
+++ b/src/components/shared/utils/contract/contract.tsx
@@ -0,0 +1,353 @@
+import React from 'react';
+import moment from 'moment';
+
+import { Localize } from '@/utils/tmp/dummy';
+
+import { unique } from '../object';
+import { isForwardStarting } from '../shortcode';
+import { capitalizeFirstLetter } from '../string/string_util';
+
+import { TContractInfo, TContractStore, TDigitsInfo, TLimitOrder, TTickItem } from './contract-types';
+
+type TGetAccuBarriersDTraderTimeout = (params: {
+ barriers_update_timestamp: number;
+ has_default_timeout: boolean;
+ tick_update_timestamp: number | null;
+ underlying: string;
+}) => number;
+
+// Trade types that are considered as vanilla financials
+export const VANILLA_FX_SYMBOLS = [
+ 'frxAUDUSD',
+ 'frxEURUSD',
+ 'frxGBPUSD',
+ 'frxUSDCAD',
+ 'frxUSDJPY',
+ 'frxXAUUSD',
+ 'frxXAGUSD',
+] as const;
+
+// animation correction time is an interval in ms between ticks receival from API and their actual visual update on the chart
+export const ANIMATION_CORRECTION_TIME = 200;
+export const DELAY_TIME_1S_SYMBOL = 500;
+// generation_interval will be provided via API later to help us distinguish between 1-second and 2-second symbols
+export const symbols_2s = ['R_10', 'R_25', 'R_50', 'R_75', 'R_100'];
+
+export const CONTRACT_TYPES = {
+ ACCUMULATOR: 'ACCU',
+ ASIAN: { UP: 'ASIANU', DOWN: 'ASIAND' },
+ CALL: 'CALL',
+ CALLE: 'CALLE',
+ CALL_BARRIER: 'CALL_BARRIER',
+ CALL_PUT_SPREAD: { CALL: 'CALLSPREAD', PUT: 'PUTSPREAD' },
+ END: { IN: 'EXPIRYRANGE', OUT: 'EXPIRYMISS' },
+ EVEN_ODD: { ODD: 'DIGITODD', EVEN: 'DIGITEVEN' },
+ EXPIRYRANGEE: 'EXPIRYRANGEE',
+ FALL: 'FALL',
+ HIGHER: 'HIGHER',
+ LB_HIGH_LOW: 'LBHIGHLOW',
+ LB_CALL: 'LBFLOATCALL',
+ LB_PUT: 'LBFLOATPUT',
+ LOWER: 'LOWER',
+ MATCH_DIFF: { MATCH: 'DIGITMATCH', DIFF: 'DIGITDIFF' },
+ MULTIPLIER: {
+ UP: 'MULTUP',
+ DOWN: 'MULTDOWN',
+ },
+ OVER_UNDER: { OVER: 'DIGITOVER', UNDER: 'DIGITUNDER' },
+ PUT: 'PUT',
+ PUTE: 'PUTE',
+ PUT_BARRIER: 'PUT_BARRIER',
+ RESET: { CALL: 'RESETCALL', PUT: 'RESETPUT' },
+ RISE: 'RISE',
+ RUN_HIGH_LOW: { HIGH: 'RUNHIGH', LOW: 'RUNLOW' },
+ STAY: { IN: 'RANGE', OUT: 'UPORDOWN' },
+ TICK_HIGH_LOW: { HIGH: 'TICKHIGH', LOW: 'TICKLOW' },
+ TOUCH: { ONE_TOUCH: 'ONETOUCH', NO_TOUCH: 'NOTOUCH' },
+ TURBOS: {
+ LONG: 'TURBOSLONG',
+ SHORT: 'TURBOSSHORT',
+ },
+ VANILLA: {
+ CALL: 'VANILLALONGCALL',
+ PUT: 'VANILLALONGPUT',
+ },
+} as const;
+export const TRADE_TYPES = {
+ ACCUMULATOR: 'accumulator',
+ ASIAN: 'asian',
+ CALL_PUT_SPREAD: 'callputspread',
+ END: 'end',
+ EVEN_ODD: 'even_odd',
+ HIGH_LOW: 'high_low',
+ LB_HIGH_LOW: 'lb_high_low',
+ LB_CALL: 'lb_call',
+ LB_PUT: 'lb_put',
+ MATCH_DIFF: 'match_diff',
+ MULTIPLIER: 'multiplier',
+ OVER_UNDER: 'over_under',
+ RESET: 'reset',
+ RISE_FALL: 'rise_fall',
+ RISE_FALL_EQUAL: 'rise_fall_equal',
+ RUN_HIGH_LOW: 'run_high_low',
+ STAY: 'stay',
+ TICK_HIGH_LOW: 'tick_high_low',
+ TOUCH: 'touch',
+ TURBOS: {
+ LONG: CONTRACT_TYPES.TURBOS.LONG.toLowerCase(),
+ SHORT: CONTRACT_TYPES.TURBOS.SHORT.toLowerCase(),
+ },
+ VANILLA: {
+ CALL: CONTRACT_TYPES.VANILLA.CALL.toLowerCase(),
+ PUT: CONTRACT_TYPES.VANILLA.PUT.toLowerCase(),
+ FX: 'vanilla_fx',
+ },
+} as const;
+
+export const getContractStatus = ({ contract_type, exit_tick_time, profit, status }: TContractInfo) => {
+ const closed_contract_status = profit && profit < 0 && exit_tick_time ? 'lost' : 'won';
+ return isAccumulatorContract(contract_type)
+ ? (status === 'open' && !exit_tick_time && 'open') || closed_contract_status
+ : status;
+};
+
+export const getFinalPrice = (contract_info: TContractInfo) => contract_info.sell_price || contract_info.bid_price;
+
+export const getIndicativePrice = (contract_info: TContractInfo) =>
+ getFinalPrice(contract_info) && isEnded(contract_info)
+ ? getFinalPrice(contract_info)
+ : Number(contract_info.bid_price);
+
+export const getCancellationPrice = (contract_info: TContractInfo) => {
+ const { cancellation: { ask_price: cancellation_price = 0 } = {} } = contract_info;
+ return cancellation_price;
+};
+
+export const isEnded = (contract_info: TContractInfo) =>
+ !!(
+ (contract_info.status && contract_info.status !== 'open') ||
+ contract_info.is_expired ||
+ contract_info.is_settleable
+ );
+
+export const isOpen = (contract_info: TContractInfo) => getContractStatus(contract_info) === 'open';
+
+export const isUserSold = (contract_info: TContractInfo) => contract_info.status === 'sold';
+
+export const isValidToCancel = (contract_info: TContractInfo) => !!contract_info.is_valid_to_cancel;
+
+export const isValidToSell = (contract_info: TContractInfo) =>
+ !isEnded(contract_info) && !isUserSold(contract_info) && !!contract_info.is_valid_to_sell;
+
+export const hasContractEntered = (contract_info: TContractInfo) => !!contract_info.entry_spot;
+
+export const hasBarrier = (contract_type = '') => /VANILLA|TURBOS|HIGH_LOW|TOUCH/i.test(contract_type);
+
+export const hasTwoBarriers = (contract_type = '') => /EXPIRY|RANGE|UPORDOWN/i.test(contract_type);
+
+export const isAccumulatorContract = (contract_type = '') => /ACCU/i.test(contract_type);
+
+export const isAccumulatorContractOpen = (contract_info: TContractInfo = {}) => {
+ return isAccumulatorContract(contract_info.contract_type) && getContractStatus(contract_info) === 'open';
+};
+
+export const isMultiplierContract = (contract_type = '') => /MULT/i.test(contract_type);
+
+export const isTouchContract = (contract_type: string) => /TOUCH/i.test(contract_type);
+
+export const isTurbosContract = (contract_type = '') => /TURBOS/i.test(contract_type);
+
+export const isVanillaContract = (contract_type = '') => /VANILLA/i.test(contract_type);
+
+export const isVanillaFxContract = (contract_type = '', symbol = '') =>
+ isVanillaContract(contract_type) && VANILLA_FX_SYMBOLS.includes(symbol as (typeof VANILLA_FX_SYMBOLS)[number]);
+
+export const isSmartTraderContract = (contract_type = '') =>
+ /RUN|EXPIRY|RANGE|UPORDOWN|ASIAN|RESET|TICK|LB/i.test(contract_type);
+
+export const isAsiansContract = (contract_type = '') => /ASIAN/i.test(contract_type);
+
+export const isLookBacksContract = (contract_type = '') => /LB/i.test(contract_type);
+
+export const isTicksContract = (contract_type = '') => /TICK/i.test(contract_type);
+
+export const isResetContract = (contract_type = '') => /RESET/i.test(contract_type);
+
+export const isCryptoContract = (underlying = '') => underlying.startsWith('cry');
+
+export const getAccuBarriersDefaultTimeout = (symbol: string) => {
+ return symbols_2s.includes(symbol) ? DELAY_TIME_1S_SYMBOL * 2 : DELAY_TIME_1S_SYMBOL;
+};
+
+export const getAccuBarriersDTraderTimeout: TGetAccuBarriersDTraderTimeout = ({
+ barriers_update_timestamp,
+ has_default_timeout,
+ tick_update_timestamp,
+ underlying,
+}) => {
+ if (has_default_timeout || !tick_update_timestamp) return getAccuBarriersDefaultTimeout(underlying);
+ const target_update_time =
+ tick_update_timestamp + getAccuBarriersDefaultTimeout(underlying) + ANIMATION_CORRECTION_TIME;
+ const difference = target_update_time - barriers_update_timestamp;
+ return difference < 0 ? 0 : difference;
+};
+
+export const getAccuBarriersForContractDetails = (contract_info: TContractInfo) => {
+ if (!isAccumulatorContract(contract_info.contract_type)) return {};
+ const is_contract_open = isOpen(contract_info);
+ const { current_spot_high_barrier, current_spot_low_barrier, high_barrier, low_barrier } = contract_info || {};
+ const accu_high_barrier = is_contract_open ? current_spot_high_barrier : high_barrier;
+ const accu_low_barrier = is_contract_open ? current_spot_low_barrier : low_barrier;
+ return { accu_high_barrier, accu_low_barrier };
+};
+
+export const getCurrentTick = (contract_info: TContractInfo) => {
+ const tick_stream = unique(contract_info.tick_stream || [], 'epoch');
+ const current_tick =
+ isDigitContract(contract_info.contract_type) || isAsiansContract(contract_info.contract_type)
+ ? tick_stream.length
+ : tick_stream.length - 1;
+ return !current_tick || current_tick < 0 ? 0 : current_tick;
+};
+
+export const getLastContractMarkerIndex = (markers: TContractStore[] = []) => {
+ const sorted_markers = [...markers].sort(
+ (a, b) => Number(b.contract_info.date_start) - Number(a.contract_info.date_start)
+ );
+ const index = sorted_markers[0].contract_info.date_start ? markers.indexOf(sorted_markers[0]) : -1;
+ return index >= 0 ? index : markers.length - 1;
+};
+
+export const getLastTickFromTickStream = (tick_stream: TTickItem[] = []) => tick_stream[tick_stream.length - 1] || {};
+
+export const isDigitContract = (contract_type = '') => /digit/i.test(contract_type);
+
+export const getDigitInfo = (digits_info: TDigitsInfo, contract_info: TContractInfo) => {
+ const { tick_stream } = contract_info;
+ const { tick_display_value, epoch } = getLastTickFromTickStream(tick_stream);
+
+ if (!tick_display_value || !epoch) return {}; // filter out empty responses
+
+ const current =
+ epoch in digits_info
+ ? {} // filter out duplicated responses
+ : createDigitInfo(tick_display_value, epoch);
+
+ return {
+ ...current,
+ };
+};
+
+export const getTotalProfit = (contract_info: TContractInfo) =>
+ Number(contract_info.bid_price) - Number(contract_info.buy_price);
+
+const createDigitInfo = (spot: string, spot_time: number) => {
+ const digit = +`${spot}`.slice(-1);
+
+ return {
+ [+spot_time]: {
+ digit,
+ spot,
+ },
+ };
+};
+
+export const getLimitOrderAmount = (limit_order?: TLimitOrder) => {
+ if (!limit_order) return { stop_loss: null, take_profit: null };
+ const {
+ stop_loss: { order_amount: stop_loss_order_amount } = {},
+ take_profit: { order_amount: take_profit_order_amount } = {},
+ } = limit_order;
+
+ return {
+ stop_loss: stop_loss_order_amount,
+ take_profit: take_profit_order_amount,
+ };
+};
+
+export const getTimePercentage = (server_time: moment.Moment, start_time: number, expiry_time: number) => {
+ const duration_from_purchase = moment.duration(moment.unix(expiry_time).diff(moment.unix(start_time)));
+ const duration_from_now = moment.duration(moment.unix(expiry_time).diff(server_time));
+ let percentage = (duration_from_now.asMilliseconds() / duration_from_purchase.asMilliseconds()) * 100;
+
+ if (percentage < 0.5) {
+ percentage = 0;
+ } else if (percentage > 100) {
+ percentage = 100;
+ }
+
+ return Math.round(percentage);
+};
+
+export const getGrowthRatePercentage = (growth_rate: number) => growth_rate * 100;
+
+export const getDisplayStatus = (contract_info: TContractInfo) => {
+ let status = 'purchased';
+ if (isEnded(contract_info)) {
+ status = getTotalProfit(contract_info) >= 0 ? 'won' : 'lost';
+ }
+ return status;
+};
+
+/**
+ * Set contract update form initial values
+ * @param {object} contract_update - contract_update response
+ * @param {object} limit_order - proposal_open_contract.limit_order response
+ */
+
+export const getContractUpdateConfig = ({ contract_update, limit_order }: TContractInfo) => {
+ const { stop_loss, take_profit } = getLimitOrderAmount(limit_order || contract_update);
+
+ return {
+ // convert stop_loss, take_profit value to string for validation to work
+ contract_update_stop_loss: stop_loss ? Math.abs(stop_loss).toString() : '',
+ contract_update_take_profit: take_profit ? take_profit.toString() : '',
+ has_contract_update_stop_loss: !!stop_loss,
+ has_contract_update_take_profit: !!take_profit,
+ };
+};
+
+export const shouldShowExpiration = (symbol = '') => symbol.startsWith('cry');
+
+export const shouldShowCancellation = (symbol = '') => !/^(cry|CRASH|BOOM|stpRNG|WLD|JD)/.test(symbol);
+
+export const getContractSubtype = (type = '') =>
+ /(VANILLALONG|TURBOS)/i.test(type)
+ ? capitalizeFirstLetter(type.replace(/(VANILLALONG|TURBOS)/i, '').toLowerCase())
+ : '';
+
+export const getLocalizedTurbosSubtype = (contract_type = '') => {
+ if (!isTurbosContract(contract_type)) return '';
+ return getContractSubtype(contract_type) === 'Long' ? (
+
+ ) : (
+
+ );
+};
+
+export const clickAndKeyEventHandler = (
+ callback?: () => void,
+ e?: React.MouseEvent | React.KeyboardEvent
+) => {
+ if (e) {
+ e.preventDefault();
+ if (e.type !== 'keydown' || (e.type === 'keydown' && (e as React.KeyboardEvent).key === 'Enter')) {
+ callback?.();
+ }
+ } else {
+ callback?.();
+ }
+};
+
+export const getSortedTradeTypes = (array: string[] = []) => {
+ if (array.includes(TRADE_TYPES.ACCUMULATOR)) {
+ return [TRADE_TYPES.ACCUMULATOR, ...array.filter(type => type !== TRADE_TYPES.ACCUMULATOR)];
+ }
+ if (array.includes(TRADE_TYPES.MULTIPLIER)) {
+ return [TRADE_TYPES.MULTIPLIER, ...array.filter(type => type !== TRADE_TYPES.MULTIPLIER)];
+ }
+ return array;
+};
+
+export const isForwardStartingBuyTransaction = (transactionType: string, shortcode: string, transactionTime: number) =>
+ transactionType === 'buy' && !!isForwardStarting(shortcode, transactionTime);
diff --git a/src/components/shared/utils/contract/index.ts b/src/components/shared/utils/contract/index.ts
new file mode 100644
index 00000000..905a4bb5
--- /dev/null
+++ b/src/components/shared/utils/contract/index.ts
@@ -0,0 +1,4 @@
+export * from './contract';
+export * from './contract-info';
+export * from './contract-types';
+export * from './trade-url-params-config';
diff --git a/src/components/shared/utils/contract/trade-url-params-config.ts b/src/components/shared/utils/contract/trade-url-params-config.ts
new file mode 100644
index 00000000..d8346410
--- /dev/null
+++ b/src/components/shared/utils/contract/trade-url-params-config.ts
@@ -0,0 +1,101 @@
+import { ActiveSymbols } from '@deriv/api-types';
+
+import { TTextValueStrings, TTradeTypesCategories } from '../constants/contract';
+import { routes } from '../routes';
+
+type TGetTradeURLParamsArgs = {
+ active_symbols?: ActiveSymbols;
+ contract_types_list?: TTradeTypesCategories;
+};
+
+type TTradeUrlParams = {
+ contractType?: string;
+ chartType?: string;
+ granularity?: number;
+ symbol?: string;
+};
+
+type TTradeURLParamsConfig = {
+ [key: string]: TTextValueStrings[];
+};
+
+const TRADE_URL_PARAMS = {
+ CHART_TYPE: 'chart_type',
+ INTERVAL: 'interval',
+ SYMBOL: 'symbol',
+ TRADE_TYPE: 'trade_type',
+};
+
+const tradeURLParamsConfig: TTradeURLParamsConfig = {
+ chartType: [
+ { text: 'area', value: 'line' },
+ { text: 'candle', value: 'candles' },
+ { text: 'hollow', value: 'hollow' },
+ { text: 'ohlc', value: 'ohlc' },
+ ],
+ interval: [
+ { text: '1t', value: '0' },
+ { text: '1m', value: '60' },
+ { text: '2m', value: '120' },
+ { text: '3m', value: '180' },
+ { text: '5m', value: '300' },
+ { text: '10m', value: '600' },
+ { text: '15m', value: '900' },
+ { text: '30m', value: '1800' },
+ { text: '1h', value: '3600' },
+ { text: '2h', value: '7200' },
+ { text: '4h', value: '14400' },
+ { text: '8h', value: '28800' },
+ { text: '1d', value: '86400' },
+ ],
+};
+
+const getParamTextByValue = (value: number | string, key: string) =>
+ tradeURLParamsConfig[key].find(interval => interval.value === value.toString())?.text ?? '';
+
+export const getTradeURLParams = ({ active_symbols = [], contract_types_list = {} }: TGetTradeURLParamsArgs = {}) => {
+ const searchParams = new URLSearchParams(window.location.search);
+ const result: TTradeUrlParams & { showModal?: boolean } = {};
+ if (searchParams.toString()) {
+ const { chart_type, interval, trade_type, symbol } = [...searchParams.entries()].reduce<{
+ [key: string]: string;
+ }>((acc, [key, value]) => ({ ...acc, [key]: value }), {});
+ const validInterval = tradeURLParamsConfig.interval.find(item => item.text === interval);
+ const validChartType = tradeURLParamsConfig.chartType.find(item => item.text === chart_type);
+ const chartTypeParam = Number(validInterval?.value) === 0 ? 'line' : validChartType?.value;
+ const isSymbolValid = active_symbols.some(item => item.symbol === symbol);
+ const contractList = Object.keys(contract_types_list).reduce((acc, key) => {
+ const categories: TTradeTypesCategories['Ups & Downs']['categories'] =
+ contract_types_list[key]?.categories || [];
+ return [...acc, ...categories.map(contract => (contract as TTextValueStrings).value)];
+ }, []);
+ const isTradeTypeValid = contractList.includes(trade_type ?? '');
+
+ if (validInterval) result.granularity = Number(validInterval.value);
+ if (validChartType) result.chartType = chartTypeParam;
+ if (isSymbolValid) result.symbol = symbol;
+ if (isTradeTypeValid) result.contractType = trade_type;
+ if (
+ (!isSymbolValid && symbol && active_symbols.length) ||
+ (!isTradeTypeValid && trade_type && contractList.length)
+ )
+ result.showModal = true;
+ }
+ return result;
+};
+
+export const setTradeURLParams = ({ contractType, symbol, chartType, granularity }: TTradeUrlParams) => {
+ const searchParams = new URLSearchParams(window.location.search);
+ chartType && searchParams.set(TRADE_URL_PARAMS.CHART_TYPE, getParamTextByValue(chartType, 'chartType'));
+ !isNaN(Number(granularity)) &&
+ searchParams.set(
+ TRADE_URL_PARAMS.INTERVAL,
+ getParamTextByValue(Number(granularity), TRADE_URL_PARAMS.INTERVAL)
+ );
+ symbol && searchParams.set(TRADE_URL_PARAMS.SYMBOL, symbol);
+ contractType && searchParams.set(TRADE_URL_PARAMS.TRADE_TYPE, contractType);
+ if (searchParams.toString() && window.location.pathname === routes.trade) {
+ const newQuery = `${window.location.pathname}?${searchParams.toString()}`;
+ window.history.replaceState({}, document.title, newQuery);
+ }
+};
diff --git a/src/components/shared/utils/currency/currency.ts b/src/components/shared/utils/currency/currency.ts
new file mode 100644
index 00000000..853c00f2
--- /dev/null
+++ b/src/components/shared/utils/currency/currency.ts
@@ -0,0 +1,333 @@
+import { deepFreeze, getPropertyValue } from '../object';
+
+export type TCurrenciesConfig = {
+ [key: string]: {
+ fractional_digits: number;
+ is_deposit_suspended?: 0 | 1;
+ is_suspended?: 0 | 1;
+ is_withdrawal_suspended?: 0 | 1;
+ name?: string;
+ stake_default?: number;
+ transfer_between_accounts?: {
+ fees?: { [key: string]: number };
+ limits: {
+ max?: number;
+ min: number;
+ [key: string]: unknown;
+ } | null;
+ limits_dxtrade?: { [key: string]: unknown };
+ limits_mt5?: { [key: string]: unknown };
+ };
+ type: string;
+ };
+};
+
+let currencies_config: TCurrenciesConfig = {};
+
+const fiat_currencies_display_order = ['USD', 'EUR', 'GBP', 'AUD'];
+const crypto_currencies_display_order = [
+ 'TUSDT',
+ 'BTC',
+ 'ETH',
+ 'LTC',
+ 'UST',
+ 'eUSDT',
+ 'BUSD',
+ 'DAI',
+ 'EURS',
+ 'IDK',
+ 'PAX',
+ 'TUSD',
+ 'USDC',
+ 'USDK',
+];
+
+export const reorderCurrencies = (
+ list: Array,
+ type = 'fiat'
+) => {
+ const new_order = type === 'fiat' ? fiat_currencies_display_order : crypto_currencies_display_order;
+
+ return list.sort((a, b) => {
+ if (new_order.indexOf(a.value) < new_order.indexOf(b.value)) {
+ return -1;
+ }
+ if (new_order.indexOf(a.value) > new_order.indexOf(b.value)) {
+ return 1;
+ }
+ return 0;
+ });
+};
+
+export const AMOUNT_MAX_LENGTH = 10;
+
+export const CURRENCY_TYPE = {
+ CRYPTO: 'crypto',
+ FIAT: 'fiat',
+} as const;
+
+export const getRoundedNumber = (number: number, currency: string) => {
+ return Number(Number(number).toFixed(getDecimalPlaces(currency)));
+};
+
+export const getFormattedText = (number: number, currency: string) => {
+ return `${addComma(number, getDecimalPlaces(currency), isCryptocurrency(currency))} ${currency}`;
+};
+
+/**
+ * @deprecated Please use 'FormatUtils.formatMoney' from '@deriv-com/utils' instead of this.
+ */
+export const formatMoney = (
+ currency_value: string,
+ amount: number | string,
+ exclude_currency?: boolean,
+ decimals = 0,
+ minimumFractionDigits = 0
+) => {
+ let money: number | string = amount;
+ if (money) money = String(money).replace(/,/g, '');
+ const sign = money && Number(money) < 0 ? '-' : '';
+ const decimal_places = decimals || getDecimalPlaces(currency_value);
+
+ money = isNaN(+money) ? 0 : Math.abs(+money);
+ if (typeof Intl !== 'undefined') {
+ const options = {
+ minimumFractionDigits: minimumFractionDigits || decimal_places,
+ maximumFractionDigits: decimal_places,
+ };
+ // TODO: [use-shared-i18n] - Use a getLanguage function to determine number format.
+ money = new Intl.NumberFormat('en', options).format(money);
+ } else {
+ money = addComma(money, decimal_places);
+ }
+
+ return sign + (exclude_currency ? '' : formatCurrency(currency_value)) + money;
+};
+
+export const formatCurrency = (currency: string) => {
+ return ` `;
+};
+
+export const addComma = (num?: number | string | null, decimal_points?: number, is_crypto?: boolean) => {
+ let number: number | string = String(num || 0).replace(/,/g, '');
+ if (typeof decimal_points !== 'undefined') {
+ number = (+number).toFixed(decimal_points);
+ }
+ if (is_crypto) {
+ number = parseFloat(String(number));
+ }
+
+ return number
+ .toString()
+ .replace(/(^|[^\w.])(\d{4,})/g, ($0, $1, $2) => $1 + $2.replace(/\d(?=(?:\d\d\d)+(?!\d))/g, '$&,'));
+};
+
+export const calcDecimalPlaces = (currency: string) => {
+ return isCryptocurrency(currency) ? getPropertyValue(CryptoConfig.get(), [currency, 'fractional_digits']) : 2;
+};
+
+export const getDecimalPlaces = (currency = '') =>
+ // need to check currencies_config[currency] exists instead of || in case of 0 value
+ currencies_config[currency]
+ ? getPropertyValue(currencies_config, [currency, 'fractional_digits'])
+ : calcDecimalPlaces(currency);
+
+export const setCurrencies = (website_status: { currencies_config: TCurrenciesConfig }) => {
+ currencies_config = website_status.currencies_config;
+};
+
+// (currency in crypto_config) is a back-up in case website_status doesn't include the currency config, in some cases where it's disabled
+export const isCryptocurrency = (currency: string) => {
+ return /crypto/i.test(getPropertyValue(currencies_config, [currency, 'type'])) || currency in CryptoConfig.get();
+};
+
+export const CryptoConfig = (() => {
+ let crypto_config: any;
+
+ // TODO: [use-shared-i18n] - Use translate function shared among apps or pass in translated names externally.
+ const initCryptoConfig = () =>
+ deepFreeze({
+ BTC: {
+ display_code: 'BTC',
+ name: 'Bitcoin',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 8,
+ },
+ BUSD: {
+ display_code: 'BUSD',
+ name: 'Binance USD',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ DAI: {
+ display_code: 'DAI',
+ name: 'Multi-Collateral DAI',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ EURS: {
+ display_code: 'EURS',
+ name: 'STATIS Euro',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ IDK: {
+ display_code: 'IDK',
+ name: 'IDK',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 0,
+ },
+ PAX: {
+ display_code: 'PAX',
+ name: 'Paxos Standard',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ TUSD: {
+ display_code: 'TUSD',
+ name: 'True USD',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ USDC: {
+ display_code: 'USDC',
+ name: 'USD Coin',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ USDK: {
+ display_code: 'USDK',
+ name: 'USDK',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ eUSDT: {
+ display_code: 'eUSDT',
+ name: 'Tether ERC20',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ tUSDT: {
+ display_code: 'tUSDT',
+ name: 'Tether TRC20',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 2,
+ },
+ BCH: {
+ display_code: 'BCH',
+ name: 'Bitcoin Cash',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 8,
+ },
+ ETH: {
+ display_code: 'ETH',
+ name: 'Ether',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 8,
+ },
+ ETC: {
+ display_code: 'ETC',
+ name: 'Ether Classic',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 8,
+ },
+ LTC: {
+ display_code: 'LTC',
+ name: 'Litecoin',
+ min_withdrawal: 0.002,
+ pa_max_withdrawal: 5,
+ pa_min_withdrawal: 0.002,
+ fractional_digits: 8,
+ },
+ UST: {
+ display_code: 'USDT',
+ name: 'Tether Omni',
+ min_withdrawal: 0.02,
+ pa_max_withdrawal: 2000,
+ pa_min_withdrawal: 10,
+ fractional_digits: 2,
+ },
+ // USB: {
+ // display_code: 'USB',
+ // name: 'Binary Coin',
+ // min_withdrawal: 0.02,
+ // pa_max_withdrawal: 2000,
+ // pa_min_withdrawal: 10,
+ // fractional_digits: 2,
+ // },
+ });
+
+ return {
+ get: () => {
+ if (!crypto_config) {
+ crypto_config = initCryptoConfig();
+ }
+ return crypto_config;
+ },
+ };
+})();
+
+export const getMinWithdrawal = (currency: string) => {
+ return isCryptocurrency(currency) ? getPropertyValue(CryptoConfig.get(), [currency, 'min_withdrawal']) || 0.002 : 1;
+};
+
+export const getTransferFee = (currency_from: string, currency_to: string) => {
+ const transfer_fee = getPropertyValue(currencies_config, [
+ currency_from,
+ 'transfer_between_accounts',
+ 'fees',
+ currency_to,
+ ]);
+ return `${typeof transfer_fee === 'undefined' ? '1' : transfer_fee}%`;
+};
+
+export const getCurrencyDisplayCode = (currency = '') => {
+ // eslint-disable-next-line
+ if (currency !== 'eUSDT' && currency !== 'tUSDT') currency = currency.toUpperCase();
+ return getPropertyValue(CryptoConfig.get(), [currency, 'display_code']) || currency;
+};
+
+export const getCurrencyName = (currency = '') =>
+ currency === 'USDT' ? 'Tether Omni' : getPropertyValue(currencies_config, [currency, 'name']) || '';
+
+export const getMinPayout = (currency: string) => {
+ return getPropertyValue(currencies_config, [currency, 'stake_default']);
+};
+
+export const getCurrencies = () => {
+ return currencies_config;
+};
+
+export type TAccount = {
+ account_type: 'real' | 'demo';
+ balance: number;
+ currency: string;
+};
diff --git a/src/components/shared/utils/currency/index.ts b/src/components/shared/utils/currency/index.ts
new file mode 100644
index 00000000..ed772321
--- /dev/null
+++ b/src/components/shared/utils/currency/index.ts
@@ -0,0 +1 @@
+export * from './currency';
diff --git a/src/components/shared/utils/date/date-time.ts b/src/components/shared/utils/date/date-time.ts
new file mode 100644
index 00000000..ced1dc08
--- /dev/null
+++ b/src/components/shared/utils/date/date-time.ts
@@ -0,0 +1,316 @@
+import moment from 'moment';
+
+import { getLanguage, localize } from '@/utils/tmp/dummy';
+
+type TExtendedMoment = typeof moment & {
+ createFromInputFallback: (config: { _d: Date }) => void;
+};
+
+// Disables moment's fallback to native Date object
+// moment will return `Invalid Date` if date cannot be parsed
+(moment as TExtendedMoment).createFromInputFallback = function (config) {
+ config._d = new Date(NaN); // eslint-disable-line no-underscore-dangle
+};
+
+// Localize moment instance with specific object
+export const initMoment = (lang: string) => {
+ const hasEnMomentLocale = ['EN', 'AR', 'BN', 'SI']; // 'AR', 'BN' & 'SI' langs have non-numeric dates ('২০২৪-০৫-০৭'), our current usage of moment requires us to make all dates numeric
+ if (!lang) return moment;
+ let locale = lang.toLowerCase().replace('_', '-');
+ if (hasEnMomentLocale.includes(lang)) locale = 'en-gb';
+ // TODO: Fix
+ return import(
+ /* @vite-ignore */
+ `moment/locale/${locale}`
+ )
+ .then(() => moment.locale(locale))
+ .catch(() => moment);
+};
+
+/**
+ * Convert epoch to moment object
+ * @param {Number} epoch
+ * @return {moment} the moment object of provided epoch
+ */
+export const epochToMoment = (epoch: number) => moment.unix(epoch).utc();
+
+/**
+ * Convert date string or epoch to moment object
+ * @param {Number} value the date in epoch format
+ * @param {String} value the date in string format
+ * @return {moment} the moment object of 'now' or the provided date epoch or string
+ */
+export const toMoment = (value?: moment.MomentInput): moment.Moment => {
+ if (!value) return moment().utc(); // returns 'now' moment object
+ if (value instanceof moment && (value as moment.Moment).isValid() && (value as moment.Moment).isUTC())
+ return value as moment.Moment; // returns if already a moment object
+ if (typeof value === 'number') return epochToMoment(value); // returns epochToMoment() if not a date
+
+ if (/invalid/i.test(moment(value).toString())) {
+ const today_moment = moment();
+ const days_in_month = today_moment.utc().daysInMonth();
+ const value_as_number = moment.utc(value, 'DD MMM YYYY').valueOf() / (1000 * 60 * 60 * 24);
+ return value_as_number > days_in_month
+ ? moment.utc(today_moment.add(value as string | number, 'd'), 'DD MMM YYYY')
+ : moment.utc(value, 'DD MMM YYYY'); // returns target date
+ }
+ return moment.utc(value);
+};
+
+export const toLocalFormat = (time: moment.MomentInput) => moment.utc(time).local().format('YYYY-MM-DD HH:mm:ss Z');
+export const getLongDate = (time: number): string => {
+ moment.locale(getLanguage().toLowerCase());
+ //need to divide to 1000 as timestamp coming from BE is in ms
+ return moment.unix(time / 1000).format('MMMM Do, YYYY');
+};
+
+/**
+ * Set specified time on moment object
+ * @param {moment} moment_obj the moment to set the time on
+ * @param {String} time 24 hours format, may or may not include seconds
+ * @return {moment} a new moment object of result
+ */
+export const setTime = (moment_obj: moment.Moment, time: string | null) => {
+ const [hour, minute, second] = time ? time.split(':') : [0, 0, 0];
+ moment_obj
+ .hour(+hour)
+ .minute(+minute || 0)
+ .second(+second || 0);
+ return moment_obj;
+};
+
+/**
+ * return the unix value of provided epoch and time
+ * @param {Number} epoch the date to update with provided time
+ * @param {String} time the time to set on the date
+ * @return {Number} unix value of the result
+ */
+export const convertToUnix = (epoch: number | string, time: string) => setTime(toMoment(epoch), time).unix();
+
+export const toGMTFormat = (time?: moment.MomentInput) =>
+ moment(time || undefined)
+ .utc()
+ .format('YYYY-MM-DD HH:mm:ss [GMT]');
+
+export const formatDate = (date?: moment.MomentInput, date_format = 'YYYY-MM-DD', should_format_null = true) =>
+ !should_format_null && date === null ? undefined : toMoment(date).format(date_format);
+
+export const formatTime = (epoch: number | string, time_format = 'HH:mm:ss [GMT]') =>
+ toMoment(epoch).format(time_format);
+
+/**
+ * return the number of days from today to date specified
+ * @param {String} date the date to calculate number of days from today
+ * @return {Number} an integer of the number of days
+ */
+export const daysFromTodayTo = (date?: string | moment.Moment) => {
+ const diff = toMoment(date).startOf('day').diff(toMoment().startOf('day'), 'days');
+ return !date || diff < 0 ? '' : diff;
+};
+
+/**
+ * return the number of days since the date specified
+ * @param {String} date the date to calculate number of days since
+ * @return {Number} an integer of the number of days
+ */
+export const daysSince = (date: string) => {
+ const diff = toMoment().startOf('day').diff(toMoment(date).startOf('day'), 'days');
+ return !date ? '' : diff;
+};
+
+/**
+ * return the number of months between two specified dates
+ */
+export const diffInMonths = (now: moment.MomentInput, then: moment.Moment) => then.diff(now, 'month');
+/**
+ * return moment duration between two dates
+ * @param {Number} epoch start time
+ * @param {Number} epoch end time
+ * @return {moment.duration} moment duration between start time and end time
+ */
+export const getDiffDuration = (start_time: number, end_time: number) =>
+ moment.duration(moment.unix(end_time).diff(moment.unix(start_time)));
+
+/** returns the DD MM YYYY format */
+export const getDateFromNow = (
+ days: string | number,
+ unit?: moment.unitOfTime.DurationConstructor,
+ format?: string
+) => {
+ const date = moment(new Date());
+ return date.add(days, unit).format(format);
+};
+
+/**
+ * return formatted duration `2 days 01:23:59`
+ * @param {moment.duration} moment duration object
+ * @return {String} formatted display string
+ */
+export const formatDuration = (duration: moment.Duration, format?: string) => {
+ const d = Math.floor(duration.asDays()); // duration.days() does not include months/years
+ const h = duration.hours();
+ const m = duration.minutes();
+ const s = duration.seconds();
+ const formatted_str = moment(0)
+ .hour(h)
+ .minute(m)
+ .seconds(s)
+ .format(format || 'HH:mm:ss');
+
+ return {
+ days: d,
+ timestamp: formatted_str,
+ };
+};
+
+/**
+ * return true if the time_str is in "HH:MM" format, else return false
+ * @param {String} time_str time
+ */
+export const isTimeValid = (time_str: string) =>
+ /^([0-9]|[0-1][0-9]|2[0-3]):([0-9]|[0-5][0-9])(:([0-9]|[0-5][0-9]))?$/.test(time_str);
+
+/**
+ * return true if the time_str's hour is between 0 and 23, else return false
+ * @param {String} time_str time
+ */
+export const isHourValid = (time_str: string) =>
+ isTimeValid(time_str) && /^([01][0-9]|2[0-3])$/.test(time_str.split(':')[0]);
+
+/**
+ * return true if the time_str's minute is between 0 and 59, else return false
+ * @param {String} time_str time
+ */
+export const isMinuteValid = (time_str: string) => isTimeValid(time_str) && /^[0-5][0-9]$/.test(time_str.split(':')[1]);
+
+/**
+ * return true if the date is typeof string and a valid moment date, else return false
+ * @param {String|moment} date date
+ */
+export const isDateValid = (date: moment.MomentInput) => moment(date, 'DD MMM YYYY').isValid();
+
+/**
+ * add the specified number of days to the given date
+ * @param {String} date date
+ * @param {Number} num_of_days number of days to add
+ */
+export const addDays = (date: string | moment.Moment, num_of_days: number) =>
+ toMoment(date).clone().add(num_of_days, 'day');
+
+/**
+ * add the specified number of weeks to the given date
+ * @param {String} date date
+ * @param {Number} num_of_weeks number of days to add
+ */
+export const addWeeks = (date: string, num_of_weeks: number) => toMoment(date).clone().add(num_of_weeks, 'week');
+
+/**
+ * add the specified number of months to the given date
+ * @param {String} date date
+ * @param {Number} num_of_months number of months to add
+ */
+export const addMonths = (date: moment.MomentInput, num_of_months: number) =>
+ toMoment(date).clone().add(num_of_months, 'month');
+
+/**
+ * add the specified number of years to the given date
+ * @param {String} date date
+ * @param {Number} num_of_years number of years to add
+ */
+export const addYears = (date: moment.MomentInput, num_of_years: number) =>
+ toMoment(date).clone().add(num_of_years, 'year');
+
+/**
+ * subtract the specified number of days from the given date
+ * @param {String} date date
+ * @param {Number} num_of_days number of days to subtract
+ */
+export const subDays = (date: moment.MomentInput, num_of_days: number) =>
+ toMoment(date).clone().subtract(num_of_days, 'day');
+
+/**
+ * subtract the specified number of months from the given date
+ * @param {String} date date
+ * @param {Number} num_of_months number of months to subtract
+ */
+export const subMonths = (date: moment.MomentInput, num_of_months: number) =>
+ toMoment(date).clone().subtract(num_of_months, 'month');
+
+/**
+ * subtract the specified number of years from the given date
+ * @param {String} date date
+ * @param {Number} num_of_years number of years to subtract
+ */
+export const subYears = (date: moment.MomentInput, num_of_years: number) =>
+ toMoment(date).clone().subtract(num_of_years, 'year');
+
+/**
+ * returns the minimum moment between the two passing parameters
+ * @param {moment|string|epoch} first datetime parameter
+ * @param {moment|string|epoch} second datetime parameter
+ */
+export const minDate = (date_1: moment.MomentInput, date_2: moment.MomentInput) =>
+ moment.min(toMoment(date_1), toMoment(date_2));
+
+/**
+ * returns a new date
+ * @param {moment|string|epoch} date date
+ */
+export const getStartOfMonth = (date: moment.MomentInput) =>
+ toMoment(date).clone().startOf('month').format('YYYY-MM-DD');
+
+/**
+ * returns miliseconds into UTC formatted string
+ * @param {Number} miliseconds miliseconds
+ * @param {String} str_format formatting using moment e.g - YYYY-MM-DD HH:mm
+ */
+export const formatMilliseconds = (miliseconds: moment.MomentInput, str_format: string, is_local_time = false) => {
+ if (is_local_time) {
+ return moment(miliseconds).format(str_format);
+ }
+ return moment.utc(miliseconds).format(str_format);
+};
+
+/**
+ * returns a new date string
+ * @param {moment|string|epoch} date parameter
+ * @param {String} from_date_format initial date format
+ * @param {String} to_date_format to date format
+ */
+export const convertDateFormat = (date: moment.MomentInput, from_date_format: string, to_date_format: string) =>
+ moment(date, from_date_format).format(to_date_format);
+
+/**
+ * Convert 24 hours format time to 12 hours formatted time.
+ * @param {String} time 24 hours format, may or may not include seconds
+ * @return {String} equivalent 12-hour time
+ */
+export const convertTimeFormat = (time: string) => {
+ const time_moment_obj = moment(time, 'HH:mm');
+ const time_hour = time_moment_obj.format('HH');
+ const time_min = time_moment_obj.format('mm');
+ const formatted_time = `${Number(time_hour) % 12 || 12}:${time_min}`;
+ const time_suffix = `${Number(time_hour) >= 12 ? 'pm' : 'am'}`;
+ return `${formatted_time} ${time_suffix}`;
+};
+
+/**
+ * Get a formatted time since a timestamp.
+ * @param {Number} timestamp in ms
+ * @return {String} '10s ago', or '1m ago', or '1h ago', or '1d ago'
+ */
+export const getTimeSince = (timestamp: number) => {
+ if (!timestamp) return '';
+ const seconds_passed = Math.floor((Date.now() - timestamp) / 1000);
+
+ if (seconds_passed < 60) {
+ return localize('{{seconds_passed}}s ago', { seconds_passed });
+ }
+ if (seconds_passed < 3600) {
+ return localize('{{minutes_passed}}m ago', { minutes_passed: Math.floor(seconds_passed / 60) });
+ }
+ if (seconds_passed < 86400) {
+ return localize('{{hours_passed}}h ago', { hours_passed: Math.floor(seconds_passed / 3600) });
+ }
+ return localize('{{days_passed}}d ago', { days_passed: Math.floor(seconds_passed / (3600 * 24)) });
+};
diff --git a/src/components/shared/utils/date/index.ts b/src/components/shared/utils/date/index.ts
new file mode 100644
index 00000000..d56b46b1
--- /dev/null
+++ b/src/components/shared/utils/date/index.ts
@@ -0,0 +1 @@
+export * from './date-time';
diff --git a/src/components/shared/utils/digital-options/digital-options.ts b/src/components/shared/utils/digital-options/digital-options.ts
new file mode 100644
index 00000000..1d3e88d8
--- /dev/null
+++ b/src/components/shared/utils/digital-options/digital-options.ts
@@ -0,0 +1,50 @@
+import { isEuCountry } from '../location';
+
+type TMessage = {
+ title: string;
+ text: string;
+ link: string;
+};
+
+type TShowError = {
+ message: string;
+ header: string;
+ redirect_label: string;
+ redirectOnClick?: (() => void) | null;
+ should_show_refresh: boolean;
+ redirect_to: string;
+ should_clear_error_on_click: boolean;
+ should_redirect?: boolean;
+};
+
+type TAccounts = {
+ residence?: string;
+ landing_company_shortcode?: string;
+};
+
+export const showDigitalOptionsUnavailableError = (
+ showError: (t: TShowError) => void,
+ message: TMessage,
+ redirectOnClick?: (() => void) | null,
+ should_redirect?: boolean,
+ should_clear_error_on_click = true
+) => {
+ const { title, text, link } = message;
+ showError({
+ message: text,
+ header: title,
+ redirect_label: link,
+ redirectOnClick,
+ should_show_refresh: false,
+ redirect_to: '/appstore/traders-hub',
+ should_clear_error_on_click,
+ should_redirect,
+ });
+};
+
+export const isEuResidenceWithOnlyVRTC = (accounts: TAccounts[]) => {
+ return (
+ accounts?.length === 1 &&
+ accounts.every(acc => isEuCountry(acc.residence ?? '') && acc.landing_company_shortcode === 'virtual')
+ );
+};
diff --git a/src/components/shared/utils/digital-options/index.ts b/src/components/shared/utils/digital-options/index.ts
new file mode 100644
index 00000000..beb190d9
--- /dev/null
+++ b/src/components/shared/utils/digital-options/index.ts
@@ -0,0 +1 @@
+export * from './digital-options';
diff --git a/src/components/shared/utils/dom/index.ts b/src/components/shared/utils/dom/index.ts
new file mode 100644
index 00000000..aa2a31e7
--- /dev/null
+++ b/src/components/shared/utils/dom/index.ts
@@ -0,0 +1 @@
+export * from './position';
diff --git a/src/components/shared/utils/dom/position.ts b/src/components/shared/utils/dom/position.ts
new file mode 100644
index 00000000..8c6568cb
--- /dev/null
+++ b/src/components/shared/utils/dom/position.ts
@@ -0,0 +1,75 @@
+type RectResult = Record<'bottom' | 'height' | 'left' | 'right' | 'top' | 'width', number>;
+
+type TGetMaxHeightByAligning = {
+ parent_rect?: RectResult;
+ child_height?: number;
+};
+
+type TGetPosition = Record<'child_el' | 'parent_el', HTMLElement | null> & {
+ preferred_alignment: string;
+ should_consider_parent_height?: boolean;
+};
+
+const getMaxHeightByAligningBottom = ({ parent_rect, child_height = 0 }: TGetMaxHeightByAligning) =>
+ (parent_rect?.top || 0) + (parent_rect?.height || 0) + child_height;
+
+const getMinHeightByAligningTop = ({ parent_rect, child_height = 0 }: TGetMaxHeightByAligning) =>
+ Number(parent_rect?.top) - child_height;
+
+export const getPosition = ({
+ preferred_alignment = 'bottom',
+ child_el,
+ parent_el,
+ should_consider_parent_height = true,
+}: TGetPosition) => {
+ const parent_rect = parent_el?.getBoundingClientRect();
+ const child_height = child_el?.clientHeight;
+ const body_rect = document.body.getBoundingClientRect();
+
+ const { top, bottom, left, width } = parent_rect || { top: 0, bottom: 0, left: 0, width: 0 };
+ const max_height = getMaxHeightByAligningBottom({ parent_rect, child_height });
+
+ const top_placement_style = {
+ bottom: body_rect.bottom - (should_consider_parent_height ? top : bottom) + 8, // add 8px extra margin for better UX
+ insetInlineStart: left,
+ width,
+ transformOrigin: 'bottom',
+ };
+
+ const bottom_placement_style = {
+ top: should_consider_parent_height ? bottom : top,
+ insetInlineStart: left,
+ width,
+ transformOrigin: 'top',
+ };
+
+ if (preferred_alignment === 'bottom') {
+ if (max_height <= body_rect.height) {
+ return {
+ style: bottom_placement_style,
+ placement: 'bottom',
+ };
+ }
+ }
+
+ const min_height = getMinHeightByAligningTop({ parent_rect, child_height });
+ if (preferred_alignment === 'top') {
+ if (min_height >= 0) {
+ return {
+ style: top_placement_style,
+ placement: 'top',
+ };
+ }
+ }
+
+ if (max_height - body_rect.height < 0 - min_height) {
+ return {
+ style: bottom_placement_style,
+ placement: 'bottom',
+ };
+ }
+ return {
+ style: top_placement_style,
+ placement: 'top',
+ };
+};
diff --git a/src/components/shared/utils/files/file-uploader-utils.ts b/src/components/shared/utils/files/file-uploader-utils.ts
new file mode 100644
index 00000000..981cb4e1
--- /dev/null
+++ b/src/components/shared/utils/files/file-uploader-utils.ts
@@ -0,0 +1,116 @@
+import { useMutation } from '@deriv/api';
+
+import { compressImg, convertToBase64, getFormatFromMIME, isImageType, TImage } from './image/image_utility';
+
+export type TSettings = Parameters>['mutate']>[0]['payload'];
+
+export type TFileObject = TSettings & {
+ filename: File['name'];
+ buffer: FileReader['result'];
+ documentFormat: string;
+ file_size: File['size'];
+};
+
+export const truncateFileName = (file: File, limit: number) => {
+ const string_limit_regex = new RegExp(`(.{${limit || 30}})..+`);
+ return file?.name?.replace(string_limit_regex, `$1….${getFileExtension(file)}`);
+};
+
+export const getFileExtension = (file: Blob) => {
+ const f = file?.type?.match(/[^/]+$/);
+ return f && f[0];
+};
+
+export const compressImageFiles = (files?: File[]) => {
+ if (!files?.length) return Promise.resolve([]);
+
+ const promises: Promise[] = [];
+ Array.from(files).forEach(file => {
+ const promise = new Promise(resolve => {
+ if (isImageType(file?.type)) {
+ convertToBase64(file).then(img => {
+ compressImg(img as TImage).then(resolve);
+ });
+ } else {
+ resolve(file);
+ }
+ });
+ promises.push(promise);
+ });
+
+ return Promise.all(promises);
+};
+
+export const readFiles = (
+ files: Blob[],
+ getFileReadErrorMessage: (t: string) => string,
+ settings?: Partial
+) => {
+ const promises: Array | { message: string }>> = [];
+
+ files.forEach(f => {
+ const fr = new FileReader();
+ const promise = new Promise | { message: string }>(resolve => {
+ fr.onload = () => {
+ const file_metadata = {
+ filename: f.name,
+ buffer: fr.result,
+ documentFormat: getFormatFromMIME(f),
+ file_size: f.size,
+ documentType: settings?.document_type ?? UPLOAD_FILE_TYPE.utility_bill,
+ documentId: settings?.document_id,
+ expirationDate: settings?.expiration_date,
+ lifetimeValid: settings?.lifetime_valid,
+ pageType: settings?.page_type,
+ proof_of_ownership: settings?.proof_of_ownership,
+ document_issuing_country: settings?.document_issuing_country,
+ };
+ resolve(file_metadata);
+ };
+
+ fr.onerror = () => {
+ resolve({
+ message:
+ typeof getFileReadErrorMessage === 'function'
+ ? getFileReadErrorMessage(f.name)
+ : `Unable to read file ${f.name}`,
+ });
+ };
+ // Reading file
+ fr.readAsArrayBuffer(f);
+ });
+
+ promises.push(promise);
+ });
+
+ return Promise.all(promises);
+};
+
+export const max_document_size = 8388608;
+
+export const supported_filetypes = 'image/png, image/jpeg, image/jpg, image/gif, application/pdf';
+
+export const UPLOAD_FILE_TYPE = Object.freeze({
+ amlglobalcheck: 'amlglobalcheck',
+ bankstatement: 'bankstatement',
+ docverification: 'docverification',
+ driverslicense: 'driverslicense',
+ driving_licence: 'driving_licence',
+ national_identity_card: 'national_identity_card',
+ other: 'other',
+ passport: 'passport',
+ power_of_attorney: 'power_of_attorney',
+ proof_of_ownership: 'proof_of_ownership',
+ proofaddress: 'proofaddress',
+ proofid: 'proofid',
+ utility_bill: 'utility_bill',
+});
+
+export const PAGE_TYPE = Object.freeze({
+ back: 'back',
+ front: 'front',
+ photo: 'photo',
+});
+
+export const getSupportedFiles = (filename: string) =>
+ /^.*\.(png|PNG|jpg|JPG|jpeg|JPEG|gif|GIF|pdf|PDF)$/.test(filename);
diff --git a/src/components/shared/utils/files/image/image_utility.ts b/src/components/shared/utils/files/image/image_utility.ts
new file mode 100644
index 00000000..28bd8da6
--- /dev/null
+++ b/src/components/shared/utils/files/image/image_utility.ts
@@ -0,0 +1,75 @@
+import 'canvas-toBlob';
+
+declare global {
+ interface Blob {
+ lastModifiedDate: number;
+ name: string;
+ }
+}
+
+export type TImage = {
+ src: string;
+ filename: string;
+};
+
+export type TFile = File & { file: Blob };
+
+const compressImg = (image: TImage): Promise =>
+ new Promise(resolve => {
+ const img = new Image();
+ img.src = image.src;
+ img.onload = () => {
+ const canvas: HTMLCanvasElement = document.createElement('canvas');
+ const canvas_res = canvas.getContext('2d');
+ if (!canvas_res || !(canvas_res instanceof CanvasRenderingContext2D)) {
+ throw new Error('Failed to get 2D context');
+ }
+ const context: CanvasRenderingContext2D = canvas_res;
+ if (img.naturalWidth > 2560) {
+ const width = 2560;
+ const scaleFactor = width / img.naturalWidth;
+ canvas.width = width;
+ canvas.height = img.naturalHeight * scaleFactor;
+ } else {
+ canvas.height = img.naturalHeight;
+ canvas.width = img.naturalWidth;
+ }
+
+ context.fillStyle = 'transparent';
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ context.save();
+ context.drawImage(img, 0, 0, canvas.width, canvas.height);
+
+ canvas.toBlob(
+ blob => {
+ const filename = image.filename.replace(/\.[^/.]+$/, '.jpg');
+ const file = new Blob([blob as BlobPart], {
+ type: 'image/jpeg',
+ });
+ file.lastModifiedDate = Date.now();
+ file.name = filename;
+ resolve(file);
+ },
+ 'image/jpeg',
+ 0.9
+ ); // <----- set quality here
+ };
+ });
+
+const convertToBase64 = (file: File) =>
+ new Promise(resolve => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onloadend = () => {
+ const result = { src: reader.result, filename: file.name };
+ resolve(result);
+ };
+ });
+
+const isImageType = (filename: string) => /(gif|jpg|jpeg|tiff|png)$/i.test(filename);
+
+const getFormatFromMIME = (file: Blob) =>
+ (file.type.split('/')[1] || (file.name.match(/\.([\w\d]+)$/) || [])[1] || '').toUpperCase();
+
+export { compressImg, convertToBase64, isImageType, getFormatFromMIME };
diff --git a/src/components/shared/utils/files/index.ts b/src/components/shared/utils/files/index.ts
new file mode 100644
index 00000000..fcd1c7a8
--- /dev/null
+++ b/src/components/shared/utils/files/index.ts
@@ -0,0 +1 @@
+export * from './file-uploader-utils';
diff --git a/src/components/shared/utils/helpers/active-symbols.ts b/src/components/shared/utils/helpers/active-symbols.ts
new file mode 100644
index 00000000..e114961f
--- /dev/null
+++ b/src/components/shared/utils/helpers/active-symbols.ts
@@ -0,0 +1,143 @@
+import { flow } from 'mobx';
+
+import { ActiveSymbols } from '@deriv/api-types';
+
+import { getLanguage, localize } from '@/utils/tmp/dummy';
+
+import { WS } from '../../services';
+import { redirectToLogin } from '../login';
+import { LocalStore } from '../storage';
+
+type TResidenceList = {
+ residence_list: {
+ disabled?: string;
+ phone_idd?: null | string;
+ selected?: string;
+ text?: string;
+ tin_format?: string[];
+ value?: string;
+ }[];
+};
+
+type TIsSymbolOpen = {
+ exchange_is_open: 0 | 1;
+};
+
+export const showUnavailableLocationError = flow(function* (showError, is_logged_in) {
+ const website_status = yield WS.wait('website_status');
+ const residence_list: TResidenceList = yield WS.residenceList();
+
+ const clients_country_code = website_status.website_status.clients_country;
+ const clients_country_text = (
+ residence_list.residence_list.find(obj_country => obj_country.value === clients_country_code) || {}
+ ).text;
+
+ const header = clients_country_text
+ ? localize('Sorry, this app is unavailable in {{clients_country}}.', { clients_country: clients_country_text })
+ : localize('Sorry, this app is unavailable in your current location.');
+
+ showError({
+ message: localize('If you have an account, log in to continue.'),
+ header,
+ redirect_label: localize('Log in'),
+ redirectOnClick: () => redirectToLogin(is_logged_in, getLanguage()),
+ should_show_refresh: false,
+ });
+});
+
+// eslint-disable-next-line default-param-last
+export const isMarketClosed = (active_symbols: ActiveSymbols = [], symbol: string) => {
+ if (!active_symbols.length) return false;
+ return active_symbols.filter(x => x.symbol === symbol)[0]
+ ? !active_symbols.filter(symbol_info => symbol_info.symbol === symbol)[0].exchange_is_open
+ : false;
+};
+
+export const pickDefaultSymbol = async (active_symbols: ActiveSymbols = []) => {
+ if (!active_symbols.length) return '';
+ const fav_open_symbol = await getFavoriteOpenSymbol(active_symbols);
+ if (fav_open_symbol) return fav_open_symbol;
+ const default_open_symbol = await getDefaultOpenSymbol(active_symbols);
+ return default_open_symbol;
+};
+
+const getFavoriteOpenSymbol = async (active_symbols: ActiveSymbols) => {
+ try {
+ const chart_favorites = LocalStore.get('cq-favorites');
+ if (!chart_favorites) return undefined;
+ const client_favorite_markets: string[] = JSON.parse(chart_favorites)['chartTitle&Comparison'];
+
+ const client_favorite_list = client_favorite_markets.map(client_fav_symbol =>
+ active_symbols.find(symbol_info => symbol_info.symbol === client_fav_symbol)
+ );
+ if (client_favorite_list) {
+ const client_first_open_symbol = client_favorite_list.filter(symbol => symbol).find(isSymbolOpen);
+ if (client_first_open_symbol) {
+ const is_symbol_offered = await isSymbolOffered(client_first_open_symbol.symbol);
+ if (is_symbol_offered) return client_first_open_symbol.symbol;
+ }
+ }
+ return undefined;
+ } catch (error) {
+ return undefined;
+ }
+};
+
+const getDefaultOpenSymbol = async (active_symbols: ActiveSymbols) => {
+ const default_open_symbol =
+ (await findSymbol(active_symbols, '1HZ100V')) ||
+ (await findFirstSymbol(active_symbols, /random_index/)) ||
+ (await findFirstSymbol(active_symbols, /major_pairs/));
+ if (default_open_symbol) return default_open_symbol.symbol;
+ return active_symbols.find(symbol_info => symbol_info.submarket === 'major_pairs')?.symbol;
+};
+
+const findSymbol = async (active_symbols: ActiveSymbols, symbol: string) => {
+ const first_symbol = active_symbols.find(symbol_info => symbol_info.symbol === symbol && isSymbolOpen(symbol_info));
+ const is_symbol_offered = await isSymbolOffered(first_symbol?.symbol);
+ if (is_symbol_offered) return first_symbol;
+ return undefined;
+};
+
+const findFirstSymbol = async (active_symbols: ActiveSymbols, pattern: RegExp) => {
+ const first_symbol = active_symbols.find(
+ symbol_info => pattern.test(symbol_info.submarket) && isSymbolOpen(symbol_info)
+ );
+ const is_symbol_offered = await isSymbolOffered(first_symbol?.symbol);
+ if (is_symbol_offered) return first_symbol;
+ return undefined;
+};
+
+type TFindFirstOpenMarket = { category?: string; subcategory?: string } | undefined;
+
+export const findFirstOpenMarket = async (
+ active_symbols: ActiveSymbols,
+ markets: string[]
+): Promise => {
+ const market = markets.shift();
+ const first_symbol = active_symbols.find(symbol_info => market === symbol_info.market && isSymbolOpen(symbol_info));
+ const is_symbol_offered = await isSymbolOffered(first_symbol?.symbol);
+ if (is_symbol_offered) return { category: first_symbol?.market, subcategory: first_symbol?.submarket };
+ else if (markets.length > 0) return findFirstOpenMarket(active_symbols, markets);
+ return undefined;
+};
+
+const isSymbolOpen = (symbol?: TIsSymbolOpen) => symbol?.exchange_is_open === 1;
+
+const isSymbolOffered = async (symbol?: string) => {
+ const r = await WS.storage.contractsFor(symbol);
+ return !['InvalidSymbol', 'InputValidationFailed'].includes(r.error?.code);
+};
+
+export type TActiveSymbols = {
+ symbol: string;
+ display_name: string;
+}[];
+
+// eslint-disable-next-line default-param-last
+export const getSymbolDisplayName = (active_symbols: TActiveSymbols = [], symbol: string) =>
+ (
+ active_symbols.find(symbol_info => symbol_info.symbol.toUpperCase() === symbol.toUpperCase()) || {
+ display_name: '',
+ }
+ ).display_name;
diff --git a/src/components/shared/utils/helpers/barrier.ts b/src/components/shared/utils/helpers/barrier.ts
new file mode 100644
index 00000000..f652844e
--- /dev/null
+++ b/src/components/shared/utils/helpers/barrier.ts
@@ -0,0 +1,30 @@
+type TContract = {
+ high_barrier?: null | string;
+ barriers?: number;
+ barrier?: null | string;
+ low_barrier?: null | string;
+ expiry_type: string;
+};
+
+type TObjectBarrier = Pick;
+
+export const buildBarriersConfig = (contract: TContract, barriers = { count: contract.barriers }) => {
+ if (!contract.barriers) {
+ return undefined;
+ }
+
+ const obj_barrier: TObjectBarrier = {};
+
+ ['barrier', 'low_barrier', 'high_barrier'].forEach(field => {
+ if (field in contract) obj_barrier[field as keyof TObjectBarrier] = contract[field as keyof TObjectBarrier];
+ });
+
+ return Object.assign(barriers || {}, {
+ [contract.expiry_type]: obj_barrier,
+ });
+};
+
+export const getBarrierPipSize = (barrier: string) => {
+ if (barrier.length < 1 || isNaN(+barrier)) return 0;
+ return barrier.split('.')[1]?.length || 0;
+};
diff --git a/src/components/shared/utils/helpers/barriers.ts b/src/components/shared/utils/helpers/barriers.ts
new file mode 100644
index 00000000..e2597249
--- /dev/null
+++ b/src/components/shared/utils/helpers/barriers.ts
@@ -0,0 +1,15 @@
+import { CONTRACT_SHADES } from '../constants';
+
+export const isBarrierSupported = (contract_type: string) => contract_type in CONTRACT_SHADES;
+
+export const barriersToString = (is_relative: boolean, ...barriers_list: number[]) =>
+ barriers_list
+ .filter(barrier => barrier !== undefined && barrier !== null)
+ .map(barrier => `${is_relative && !/^[+-]/.test(barrier.toString()) ? '+' : ''}${barrier}`);
+
+export const removeBarrier = (barriers: { [key: string]: string | number }[], key: string) => {
+ const index = barriers.findIndex(b => b.key === key);
+ if (index > -1) {
+ barriers.splice(index, 1);
+ }
+};
diff --git a/src/components/shared/utils/helpers/chart-notifications.tsx b/src/components/shared/utils/helpers/chart-notifications.tsx
new file mode 100644
index 00000000..a8182c91
--- /dev/null
+++ b/src/components/shared/utils/helpers/chart-notifications.tsx
@@ -0,0 +1,8 @@
+import { Localize, localize } from '@/utils/tmp/dummy';
+
+export const switch_to_tick_chart = {
+ key: 'switch_to_tick_chart',
+ header: localize('This chart display is not ideal for tick contracts'),
+ message: ,
+ type: 'info',
+};
diff --git a/src/components/shared/utils/helpers/details.ts b/src/components/shared/utils/helpers/details.ts
new file mode 100644
index 00000000..99d7fc13
--- /dev/null
+++ b/src/components/shared/utils/helpers/details.ts
@@ -0,0 +1,109 @@
+import moment from 'moment';
+
+import { localize } from '@/utils/tmp/dummy';
+
+import { TContractInfo } from '../contract';
+import { epochToMoment, formatMilliseconds, getDiffDuration } from '../date';
+
+type TUnitMap = {
+ name_plural?: string;
+ name_singular?: string;
+ name?: string;
+};
+
+export const getDurationUnitValue = (obj_duration: moment.Duration) => {
+ const duration_ms = obj_duration.asMilliseconds() / 1000;
+ // Check with isEndTime to find out if value of duration has decimals
+ // for days we do not require precision for End Time value since users cannot select with timepicker if not in same day
+ // Seconds is the smallest End Time duration displayed thus we can keep the same formatting as normal Duration
+
+ if (duration_ms >= 86400000) {
+ const duration = duration_ms / (1000 * 60 * 60 * 24);
+ return Math.floor(duration);
+ } else if (duration_ms >= 3600000 && duration_ms < 86400000) {
+ const duration = duration_ms / (1000 * 60 * 60);
+ const is_end_time = isEndTime(duration);
+ const has_seconds = isEndTime(duration_ms / (1000 * 60));
+ const string_format = has_seconds ? 'HH[h] mm[m] ss[s]' : 'HH[h] mm[m]';
+
+ return is_end_time
+ ? formatMilliseconds(duration_ms, string_format)
+ : Math.floor(duration_ms / (1000 * 60 * 60));
+ } else if (duration_ms >= 60000 && duration_ms < 3600000) {
+ const duration = duration_ms / (1000 * 60);
+ const is_end_time = isEndTime(duration);
+ return is_end_time ? formatMilliseconds(duration_ms, 'mm[m] ss[s]') : Math.floor(duration_ms / (1000 * 60));
+ } else if (duration_ms >= 1000 && duration_ms < 60000) {
+ return Math.floor(duration_ms / 1000);
+ }
+ return Math.floor(duration_ms / 1000);
+};
+
+export const isEndTime = (duration: number) => duration % 1 !== 0;
+
+export const getUnitMap = () => {
+ return {
+ d: { name_plural: localize('days'), name_singular: localize('day') },
+ h: { name_plural: localize('hours'), name_singular: localize('hour') },
+ m: { name_plural: localize('minutes'), name_singular: localize('minute') },
+ s: { name: localize('seconds') },
+ t: { name_plural: localize('ticks'), name_singular: localize('tick') },
+ } as { [key: string]: TUnitMap };
+};
+
+const TIME = {
+ SECOND: 1000,
+ MINUTE: 60000,
+ HOUR: 3600000,
+ DAY: 86400000,
+} as const;
+
+export const getDurationUnitText = (obj_duration: moment.Duration, should_ignore_end_time?: boolean) => {
+ const unit_map = getUnitMap();
+ const duration_ms = obj_duration.asMilliseconds() / TIME.SECOND;
+ // return empty suffix string if duration is End Time set except for days and seconds, refer to L18 and L19
+
+ if (duration_ms >= TIME.DAY) {
+ const days_value = duration_ms / TIME.DAY;
+ return days_value <= 2 ? unit_map.d.name_singular : unit_map.d.name_plural;
+ }
+ if (duration_ms >= TIME.HOUR && duration_ms < TIME.DAY) {
+ if (!should_ignore_end_time && isEndTime(duration_ms / TIME.HOUR)) return '';
+ return duration_ms === TIME.HOUR ? unit_map.h.name_singular : unit_map.h.name_plural;
+ }
+ if (duration_ms >= TIME.MINUTE && duration_ms < TIME.HOUR) {
+ if (!should_ignore_end_time && isEndTime(duration_ms / TIME.MINUTE)) return '';
+ return duration_ms === TIME.MINUTE ? unit_map.m.name_singular : unit_map.m.name_plural;
+ }
+ if (duration_ms >= TIME.SECOND && duration_ms < TIME.MINUTE) {
+ return unit_map.s.name;
+ }
+ return unit_map.s.name;
+};
+
+export const formatResetDuration = (contract_info: TContractInfo) => {
+ const time_duration = getUnitMap();
+ const duration_ms = getDurationPeriod(contract_info).asMilliseconds() / TIME.SECOND / 2;
+ const reset_hours =
+ duration_ms === TIME.HOUR ? `h [${time_duration.h.name_singular}] ` : `h [${time_duration.h.name_plural}] `;
+ const reset_minutes =
+ duration_ms === TIME.MINUTE ? `m [${time_duration.m.name_singular}] ` : `m [${time_duration.m.name_plural}] `;
+ const reset_seconds = duration_ms % TIME.MINUTE === 0 ? '' : `s [${time_duration.s.name}]`;
+
+ return moment
+ .utc(moment.duration(duration_ms, 'milliseconds').asMilliseconds())
+ .format(
+ `${duration_ms >= TIME.HOUR ? reset_hours : ''}${
+ duration_ms >= TIME.MINUTE && duration_ms % TIME.HOUR !== 0 ? reset_minutes : ''
+ }${reset_seconds}`.trim()
+ );
+};
+
+export const getDurationPeriod = (contract_info: TContractInfo) =>
+ getDiffDuration(
+ +epochToMoment(contract_info.date_start || contract_info.purchase_time || 0),
+ +epochToMoment(contract_info.date_expiry || 0)
+ );
+
+export const getDurationTime = (contract_info: TContractInfo) =>
+ contract_info.tick_count ? contract_info.tick_count : getDurationUnitValue(getDurationPeriod(contract_info));
diff --git a/src/components/shared/utils/helpers/duration.ts b/src/components/shared/utils/helpers/duration.ts
new file mode 100644
index 00000000..5b9980ab
--- /dev/null
+++ b/src/components/shared/utils/helpers/duration.ts
@@ -0,0 +1,197 @@
+import { localize } from '@/utils/tmp/dummy';
+
+import { toMoment } from '../date';
+
+type TContract = {
+ max_contract_duration: string;
+ min_contract_duration: string;
+ expiry_type: string;
+ start_type: string;
+};
+
+type TMaxMin = {
+ min: number;
+ max: number;
+};
+
+type TUnit = {
+ text: string;
+ value: string;
+};
+
+export type TDurations = {
+ min_max: {
+ spot: Partial>;
+ forward: Partial>;
+ };
+ units_display: Partial>;
+};
+
+type TDurationMinMax = {
+ [key: string]: {
+ max: string | number;
+ min: string | number;
+ };
+};
+
+const getDurationMaps = () => ({
+ t: { display: localize('Ticks'), order: 1, to_second: null },
+ s: { display: localize('Seconds'), order: 2, to_second: 1 },
+ m: { display: localize('Minutes'), order: 3, to_second: 60 },
+ h: { display: localize('Hours'), order: 4, to_second: 60 * 60 },
+ d: { display: localize('Days'), order: 5, to_second: 60 * 60 * 24 },
+});
+
+export const buildDurationConfig = (
+ contract: TContract,
+ durations: TDurations = { min_max: { spot: {}, forward: {} }, units_display: {} }
+) => {
+ type TDurationMaps = keyof typeof duration_maps;
+ durations.units_display[contract.start_type as keyof typeof durations.units_display] =
+ durations.units_display[contract.start_type as keyof typeof durations.units_display] || [];
+
+ const duration_min_max = durations.min_max[contract.start_type as keyof typeof durations.min_max];
+ const obj_min = getDurationFromString(contract.min_contract_duration);
+ const obj_max = getDurationFromString(contract.max_contract_duration);
+
+ durations.min_max[contract.start_type as keyof typeof durations.min_max][
+ contract.expiry_type as keyof typeof duration_min_max
+ ] = {
+ min: convertDurationUnit(obj_min.duration, obj_min.unit, 's') || 0,
+ max: convertDurationUnit(obj_max.duration, obj_max.unit, 's') || 0,
+ };
+
+ const arr_units: string[] = [];
+ durations?.units_display?.[contract.start_type as keyof typeof durations.units_display]?.forEach?.(obj => {
+ arr_units.push(obj.value);
+ });
+
+ const duration_maps = getDurationMaps();
+
+ if (/^(?:tick|daily)$/.test(contract.expiry_type)) {
+ if (arr_units.indexOf(obj_min.unit) === -1) {
+ arr_units.push(obj_min.unit);
+ }
+ } else {
+ Object.keys(duration_maps).forEach(u => {
+ if (
+ u !== 'd' && // when the expiray_type is intraday, the supported units are seconds, minutes and hours.
+ arr_units.indexOf(u) === -1 &&
+ duration_maps[u as TDurationMaps].order >= duration_maps[obj_min.unit as TDurationMaps].order &&
+ duration_maps[u as TDurationMaps].order <= duration_maps[obj_max.unit as TDurationMaps].order
+ ) {
+ arr_units.push(u);
+ }
+ });
+ }
+
+ durations.units_display[contract.start_type as keyof typeof durations.units_display] = arr_units
+ .sort((a, b) => (duration_maps[a as TDurationMaps].order > duration_maps[b as TDurationMaps].order ? 1 : -1))
+ .reduce((o, c) => [...o, { text: duration_maps[c as TDurationMaps].display, value: c }], [] as TUnit[]);
+ return durations;
+};
+
+export const convertDurationUnit = (value: number, from_unit: string, to_unit: string) => {
+ if (!value || !from_unit || !to_unit || isNaN(value)) {
+ return null;
+ }
+
+ const duration_maps = getDurationMaps();
+
+ if (from_unit === to_unit || duration_maps[from_unit as keyof typeof duration_maps].to_second === null) {
+ return value;
+ }
+
+ return (
+ (value * (duration_maps[from_unit as keyof typeof duration_maps]?.to_second ?? 1)) /
+ (duration_maps[to_unit as keyof typeof duration_maps]?.to_second ?? 1)
+ );
+};
+
+const getDurationFromString = (duration_string: string) => {
+ const duration = duration_string.toString().match(/[a-zA-Z]+|[0-9]+/g) || '';
+ return {
+ duration: +duration[0], // converts string to numbers
+ unit: duration[1],
+ };
+};
+
+// TODO will change this after the global stores types get ready
+export const getExpiryType = (store: any) => {
+ const { duration_unit, expiry_date, expiry_type, duration_units_list } = store;
+ const server_time = store.root_store.common.server_time;
+
+ const duration_is_day = expiry_type === 'duration' && duration_unit === 'd';
+ const expiry_is_after_today =
+ expiry_type === 'endtime' &&
+ ((toMoment(expiry_date) as unknown as moment.Moment).isAfter(
+ toMoment(server_time) as unknown as moment.MomentInput,
+ 'day'
+ ) ||
+ !hasIntradayDurationUnit(duration_units_list));
+
+ let contract_expiry_type = 'daily';
+ if (!duration_is_day && !expiry_is_after_today) {
+ contract_expiry_type = duration_unit === 't' ? 'tick' : 'intraday';
+ }
+
+ return contract_expiry_type;
+};
+
+export const convertDurationLimit = (value: number, unit: string) => {
+ if (!(value >= 0) || !unit || !Number.isInteger(value)) {
+ return null;
+ }
+
+ if (unit === 'm') {
+ const minute = value / 60;
+ return minute >= 1 ? Math.floor(minute) : 1;
+ } else if (unit === 'h') {
+ const hour = value / (60 * 60);
+ return hour >= 1 ? Math.floor(hour) : 1;
+ } else if (unit === 'd') {
+ const day = value / (60 * 60 * 24);
+ return day >= 1 ? Math.floor(day) : 1;
+ }
+
+ return value;
+};
+
+export const hasIntradayDurationUnit = (duration_units_list: TUnit[]) => {
+ return duration_units_list.some(unit => ['m', 'h'].indexOf(unit.value) !== -1);
+};
+/**
+ * On switching symbols, end_time value of volatility indices should be set to today
+ *
+ * @param {String} symbol
+ * @param {String | null} expiry_type
+ * @returns {*}
+ */
+export const resetEndTimeOnVolatilityIndices = (symbol: string, expiry_type: string | null) =>
+ /^R_/.test(symbol) && expiry_type === 'endtime' ? toMoment(null).format('DD MMM YYYY') : null;
+
+export const getDurationMinMaxValues = (
+ duration_min_max: TDurationMinMax,
+ contract_expiry_type: string,
+ duration_unit: string
+) => {
+ if (!duration_min_max[contract_expiry_type]) return [];
+ const max_value = convertDurationLimit(+duration_min_max[contract_expiry_type].max, duration_unit);
+ const min_value = convertDurationLimit(+duration_min_max[contract_expiry_type].min, duration_unit);
+
+ return [min_value, max_value];
+};
+
+const formatDisplayedTime = (time_unit: number) => (time_unit < 10 ? `0${time_unit}` : time_unit);
+
+export const formatDurationTime = (time?: number) => {
+ if (time && !isNaN(time)) {
+ const minutes = Math.floor(time / 60);
+ const format_minutes = formatDisplayedTime(minutes);
+ const seconds = Math.floor(time % 60);
+ const format_seconds = formatDisplayedTime(seconds);
+ return `${format_minutes}:${format_seconds}`;
+ }
+
+ return '00:00';
+};
diff --git a/src/components/shared/utils/helpers/format-response.ts b/src/components/shared/utils/helpers/format-response.ts
new file mode 100644
index 00000000..472f0688
--- /dev/null
+++ b/src/components/shared/utils/helpers/format-response.ts
@@ -0,0 +1,146 @@
+import { GetSettings, ProfitTable, ResidenceList, Statement } from '@deriv/api-types';
+
+import { getContractTypeFeatureFlag, IDV_ERROR_STATUS, ONFIDO_ERROR_STATUS, STATUS_CODES } from '../constants';
+import { TContractInfo } from '../contract';
+import { extractInfoFromShortcode, isHighLow } from '../shortcode';
+import { LocalStore } from '../storage';
+
+import { getSymbolDisplayName, TActiveSymbols } from './active-symbols';
+import { getMarketInformation } from './market-underlying';
+
+export const filterDisabledPositions = (
+ position:
+ | TContractInfo
+ | NonNullable[number]
+ | NonNullable[number]
+) => {
+ const { contract_type, shortcode } = position as TContractInfo;
+ const type = contract_type ?? extractInfoFromShortcode(shortcode ?? '').category?.toUpperCase() ?? '';
+ return Object.entries(LocalStore.getObject('FeatureFlagsStore')?.data ?? {}).every(
+ ([key, value]) => !!value || key !== getContractTypeFeatureFlag(type, isHighLow({ shortcode }))
+ );
+};
+
+export const formatPortfolioPosition = (
+ portfolio_pos: TContractInfo,
+ // eslint-disable-next-line default-param-last
+ active_symbols: TActiveSymbols = [],
+ indicative?: number
+) => {
+ const purchase = portfolio_pos.buy_price;
+ const payout = portfolio_pos.payout;
+ const display_name = getSymbolDisplayName(
+ active_symbols,
+ getMarketInformation(portfolio_pos.shortcode || '').underlying
+ );
+ const transaction_id =
+ portfolio_pos.transaction_id || (portfolio_pos.transaction_ids && portfolio_pos.transaction_ids.buy);
+
+ return {
+ contract_info: portfolio_pos,
+ details: portfolio_pos.longcode?.replace(/\n/g, ' '),
+ display_name,
+ id: portfolio_pos.contract_id,
+ indicative: (indicative && isNaN(indicative)) || !indicative ? 0 : indicative,
+ payout,
+ purchase,
+ reference: Number(transaction_id),
+ type: portfolio_pos.contract_type,
+ contract_update: portfolio_pos.limit_order,
+ };
+};
+
+export type TIDVErrorStatus = keyof typeof IDV_ERROR_STATUS;
+export type TOnfidoErrorStatus = keyof typeof ONFIDO_ERROR_STATUS;
+
+const isVerifiedOrNone = (errors: Array, status_code: string, is_high_risk?: boolean) => {
+ return (
+ errors.length === 0 &&
+ (status_code === STATUS_CODES.NONE || status_code === STATUS_CODES.VERIFIED) &&
+ !is_high_risk
+ );
+};
+
+export const isIDVReportNotAvailable = (idv: Record) =>
+ 'report_available' in idv && idv?.report_available === 0;
+
+const getIDVErrorStatus = (errors: Array, is_report_not_available?: boolean) => {
+ const status: Array = [];
+ errors.forEach(error => {
+ const error_key: TIDVErrorStatus = IDV_ERROR_STATUS[error]?.code;
+ if (error_key) {
+ status.push(error_key);
+ }
+ });
+ if (
+ is_report_not_available &&
+ (status.includes(IDV_ERROR_STATUS.NameMismatch.code) || status.includes(IDV_ERROR_STATUS.DobMismatch.code))
+ ) {
+ return IDV_ERROR_STATUS.ReportNotAvailable.code;
+ }
+
+ if (status.includes(IDV_ERROR_STATUS.Failed.code)) {
+ return IDV_ERROR_STATUS.Failed.code;
+ }
+
+ if (status.includes(IDV_ERROR_STATUS.NameMismatch.code) && status.includes(IDV_ERROR_STATUS.DobMismatch.code)) {
+ return IDV_ERROR_STATUS.NameDobMismatch.code;
+ }
+ return status[0] ?? IDV_ERROR_STATUS.Failed.code;
+};
+
+// formatIDVError is parsing errors messages from BE (strings) and returns error codes for using it on FE
+export const formatIDVError = (
+ errors: Array,
+ status_code: string,
+ is_high_risk?: boolean,
+ is_report_not_available?: boolean
+) => {
+ /**
+ * Check required incase of DIEL client
+ */
+ if (isVerifiedOrNone(errors, status_code, is_high_risk)) {
+ return null;
+ }
+
+ if (is_high_risk) {
+ if (status_code === STATUS_CODES.NONE) {
+ return null;
+ } else if (status_code === STATUS_CODES.VERIFIED) {
+ return IDV_ERROR_STATUS.HighRisk.code;
+ }
+ }
+
+ if (status_code === STATUS_CODES.EXPIRED) {
+ return IDV_ERROR_STATUS.Expired.code;
+ }
+
+ return getIDVErrorStatus(errors, is_report_not_available);
+};
+
+export const formatOnfidoError = (status_code: string, errors: Array = []) => {
+ if (status_code === STATUS_CODES.EXPIRED) {
+ return [ONFIDO_ERROR_STATUS.Expired.code, ...errors];
+ }
+ return errors;
+};
+
+export const getOnfidoError = (error: TOnfidoErrorStatus) => {
+ return ONFIDO_ERROR_STATUS[error]?.message ?? '';
+};
+
+export const getIDVError = (error: TIDVErrorStatus) => {
+ return IDV_ERROR_STATUS[error]?.message ?? '';
+};
+
+export const isVerificationServiceSupported = (
+ residence_list: ResidenceList,
+ account_settings: GetSettings,
+ service: 'idv' | 'onfido'
+): boolean => {
+ const citizen = account_settings?.citizen || account_settings?.country_code;
+ if (!citizen) return false;
+ const citizen_data = residence_list.find(item => item.value === citizen);
+
+ return !!citizen_data?.identity?.services?.[service]?.is_country_supported;
+};
diff --git a/src/components/shared/utils/helpers/index.ts b/src/components/shared/utils/helpers/index.ts
new file mode 100644
index 00000000..7b613284
--- /dev/null
+++ b/src/components/shared/utils/helpers/index.ts
@@ -0,0 +1,12 @@
+export * from './active-symbols';
+export * from './barrier';
+export * from './barriers';
+export * from './chart-notifications';
+export * from './details';
+export * from './duration';
+export * from './format-response';
+export * from './logic';
+export * from './market-underlying';
+export * from './portfolio-notifications';
+export * from './start-date';
+export * from './validation-rules';
diff --git a/src/components/shared/utils/helpers/logic.ts b/src/components/shared/utils/helpers/logic.ts
new file mode 100644
index 00000000..0579d2b8
--- /dev/null
+++ b/src/components/shared/utils/helpers/logic.ts
@@ -0,0 +1,96 @@
+import moment from 'moment';
+
+import { AccountListResponse, TickSpotData, WebsiteStatus } from '@deriv/api-types';
+
+import { getSupportedContracts } from '../constants/contract';
+import { isAccumulatorContract, isOpen, isUserSold } from '../contract';
+import { TContractInfo, TContractStore } from '../contract/contract-types';
+import { isEmptyObject } from '../object';
+
+type TIsSoldBeforeStart = Required>;
+
+export const sortApiData = (arr: AccountListResponse[]) => {
+ return arr.slice().sort((a, b) => {
+ const loginA = a?.login;
+ const loginB = b?.login;
+
+ if (loginA && loginB) {
+ if (loginA < loginB) {
+ return -1;
+ }
+ if (loginA > loginB) {
+ return 1;
+ }
+ }
+ return 0;
+ });
+};
+
+export const isContractElapsed = (contract_info: TContractInfo, tick?: null | TickSpotData) => {
+ if (isEmptyObject(tick) || isEmptyObject(contract_info)) return false;
+ const end_time = getEndTime(contract_info) || 0;
+ if (end_time && tick && tick.epoch) {
+ const seconds = moment.duration(moment.unix(tick.epoch).diff(moment.unix(end_time))).asSeconds();
+ return seconds >= 2;
+ }
+ return false;
+};
+
+export const isEndedBeforeCancellationExpired = (contract_info: TContractInfo) => {
+ const end_time = getEndTime(contract_info) || 0;
+ return !!(contract_info.cancellation?.date_expiry && end_time < contract_info.cancellation.date_expiry);
+};
+
+export const isSoldBeforeStart = (contract_info: TIsSoldBeforeStart) =>
+ contract_info.sell_time && +contract_info.sell_time < +contract_info.date_start;
+
+export const hasContractStarted = (contract_info?: TContractInfo) =>
+ Number(contract_info?.current_spot_time) > Number(contract_info?.date_start);
+
+export const isUserCancelled = (contract_info: TContractInfo) => contract_info.status === 'cancelled';
+
+export const getEndTime = (contract_info: TContractInfo) => {
+ const {
+ contract_type,
+ exit_tick_time,
+ date_expiry,
+ is_expired,
+ is_path_dependent,
+ sell_time,
+ tick_count: is_tick_contract,
+ } = contract_info;
+
+ const is_finished = !isOpen(contract_info) && (is_expired || isAccumulatorContract(contract_type));
+
+ if (!is_finished && !isUserSold(contract_info) && !isUserCancelled(contract_info)) return undefined;
+
+ if (isUserSold(contract_info) && sell_time) {
+ return sell_time > Number(date_expiry) ? date_expiry : sell_time;
+ } else if (!is_tick_contract && sell_time && sell_time > Number(date_expiry)) {
+ return date_expiry;
+ }
+
+ return Number(date_expiry) > Number(exit_tick_time) && !Number(is_path_dependent) ? date_expiry : exit_tick_time;
+};
+
+export const getBuyPrice = (contract_store: TContractStore) => {
+ return contract_store.contract_info.buy_price;
+};
+
+/**
+ * Checks if the server is currently down or updating.
+ *
+ * @param {WebsiteStatusResponse} response - The response object containing the status of the website.
+ * @returns {boolean} True if the website status is 'down' or 'updating', false otherwise.
+ */
+export const checkServerMaintenance = (website_status: WebsiteStatus | undefined | null) => {
+ const { site_status = '' } = website_status || {};
+ return site_status === 'down' || site_status === 'updating';
+};
+
+export const isContractSupportedAndStarted = (symbol: string, contract_info?: TContractInfo) =>
+ !!contract_info &&
+ symbol === contract_info.underlying &&
+ //Added check for unsupported and forward starting contracts, which have not started yet
+ !!getSupportedContracts()[contract_info?.contract_type as keyof ReturnType] &&
+ hasContractStarted(contract_info);
diff --git a/src/components/shared/utils/helpers/market-underlying.ts b/src/components/shared/utils/helpers/market-underlying.ts
new file mode 100644
index 00000000..c18b0d55
--- /dev/null
+++ b/src/components/shared/utils/helpers/market-underlying.ts
@@ -0,0 +1,66 @@
+import { localize } from '@/utils/tmp/dummy';
+
+import { getContractConfig, getMarketNamesMap } from '../constants/contract';
+import { TContractOptions } from '../contract/contract-types';
+
+type TTradeConfig = {
+ button_name?: JSX.Element;
+ name: JSX.Element;
+ position: string;
+ main_title?: JSX.Element;
+};
+
+type TMarketInfo = {
+ category: string;
+ underlying: string;
+};
+
+export const getContractDurationType = (longcode: string, shortcode?: string): string => {
+ if (shortcode && /^(MULTUP|MULTDOWN)/.test(shortcode)) return '';
+
+ const duration_pattern = /ticks|tick|seconds|minutes|minute|hour|hours/;
+ const extracted = duration_pattern.exec(longcode);
+ if (extracted !== null) {
+ const duration_type = extracted[0];
+ const duration_text = duration_type[0].toUpperCase() + duration_type.slice(1);
+ return duration_text.endsWith('s') ? duration_text : `${duration_text}s`;
+ }
+ return localize('Days');
+};
+
+/**
+ * Fetch market information from shortcode
+ * @param shortcode: string
+ * @returns {{underlying: string, category: string}}
+ */
+
+// TODO: Combine with extractInfoFromShortcode function in shared, both are currently used
+export const getMarketInformation = (shortcode: string): TMarketInfo => {
+ const market_info: TMarketInfo = {
+ category: '',
+ underlying: '',
+ };
+
+ const pattern = /^([A-Z]+)_((1HZ[0-9-V]+)|((CRASH|BOOM)[0-9\d]+[A-Z]?)|(OTC_[A-Z0-9]+)|R_[\d]{2,3}|[A-Z]+)/;
+ const extracted = pattern.exec(shortcode);
+ if (extracted !== null) {
+ market_info.category = extracted[1].toLowerCase();
+ market_info.underlying = extracted[2];
+ }
+
+ return market_info;
+};
+
+export const getMarketName = (underlying: string) =>
+ underlying ? getMarketNamesMap()[underlying.toUpperCase() as keyof typeof getMarketNamesMap] : null;
+
+export const getTradeTypeName = (category: string, options: TContractOptions = {}) => {
+ const { isHighLow = false, showButtonName = false, showMainTitle = false } = options;
+
+ const trade_type =
+ category &&
+ (getContractConfig(isHighLow)[category.toUpperCase() as keyof typeof getContractConfig] as TTradeConfig);
+ if (!trade_type) return null;
+ if (showMainTitle) return trade_type?.main_title ?? '';
+ return (showButtonName && trade_type.button_name) || trade_type.name || null;
+};
diff --git a/src/components/shared/utils/helpers/portfolio-notifications.tsx b/src/components/shared/utils/helpers/portfolio-notifications.tsx
new file mode 100644
index 00000000..ecbf5263
--- /dev/null
+++ b/src/components/shared/utils/helpers/portfolio-notifications.tsx
@@ -0,0 +1,32 @@
+import Money from '@/components/shared_ui/money';
+import { Localize, localize } from '@/utils/tmp/dummy';
+
+export const contractSold = (currency: string, sold_for: number | string, Money: React.ElementType) => ({
+ key: 'contract_sold',
+ header: localize('Contract sold'),
+ message: (
+ ]}
+ />
+ ),
+ type: 'contract_sold',
+ size: 'small',
+ should_hide_close_btn: true,
+ is_auto_close: true,
+});
+
+export const contractCancelled = () => ({
+ key: 'contract_sold',
+ header: localize('Contract cancelled'),
+ message: (
+ ]}
+ />
+ ),
+ type: 'contract_sold',
+ size: 'small',
+ should_hide_close_btn: true,
+ is_auto_close: true,
+});
diff --git a/src/components/shared/utils/helpers/start-date.ts b/src/components/shared/utils/helpers/start-date.ts
new file mode 100644
index 00000000..dc1d1c7c
--- /dev/null
+++ b/src/components/shared/utils/helpers/start-date.ts
@@ -0,0 +1,39 @@
+import moment from 'moment';
+
+import { ContractsFor } from '@deriv/api-types';
+
+import { toMoment } from '../date';
+
+type TConfig = {
+ text: string;
+ value: number;
+ sessions: {
+ open: moment.Moment;
+ close: moment.Moment;
+ }[];
+}[];
+
+export const buildForwardStartingConfig = (
+ contract: ContractsFor['available'][number],
+ forward_starting_dates?: TConfig
+) => {
+ const forward_starting_config: TConfig = [];
+
+ if ((contract.forward_starting_options || []).length) {
+ (contract.forward_starting_options ?? []).forEach(option => {
+ const duplicated_option = forward_starting_config.find(opt => opt.value === parseInt(option.date ?? ''));
+ const current_session = { open: toMoment(option.open), close: toMoment(option.close) };
+ if (duplicated_option) {
+ duplicated_option.sessions.push(current_session);
+ } else {
+ forward_starting_config.push({
+ text: toMoment(option.date).format('ddd - DD MMM, YYYY'),
+ value: parseInt(option.date ?? ''),
+ sessions: [current_session],
+ });
+ }
+ });
+ }
+
+ return forward_starting_config.length ? forward_starting_config : forward_starting_dates;
+};
diff --git a/src/components/shared/utils/helpers/validation-rules.ts b/src/components/shared/utils/helpers/validation-rules.ts
new file mode 100644
index 00000000..fd9db5c2
--- /dev/null
+++ b/src/components/shared/utils/helpers/validation-rules.ts
@@ -0,0 +1,75 @@
+import { localize } from '@/utils/tmp/dummy';
+
+import { getTotalProfit } from '../contract';
+import { TContractStore } from '../contract/contract-types';
+
+import { getBuyPrice } from './logic';
+
+type TOptions = {
+ message?: string;
+ min?: number;
+ max?: number;
+};
+
+export const getContractValidationRules = () => ({
+ has_contract_update_stop_loss: {
+ trigger: 'contract_update_stop_loss',
+ },
+ contract_update_stop_loss: {
+ rules: [
+ [
+ 'req',
+ {
+ condition: (contract_store: TContractStore) => !contract_store.contract_update_stop_loss,
+ message: localize('Please enter a stop loss amount.'),
+ },
+ ],
+ [
+ 'custom',
+ {
+ func: (value: number, options: TOptions, contract_store: TContractStore) => {
+ const profit = getTotalProfit(contract_store.contract_info);
+ return !(profit < 0 && -value > profit);
+ },
+ message: localize("Please enter a stop loss amount that's higher than the current potential loss."),
+ },
+ ],
+ [
+ 'custom',
+ {
+ func: (value: number, options: TOptions, contract_store: TContractStore) => {
+ const stake = getBuyPrice(contract_store);
+ return value < Number(stake) + 1;
+ },
+ message: localize('Invalid stop loss. Stop loss cannot be more than stake.'),
+ },
+ ],
+ ],
+ },
+ has_contract_update_take_profit: {
+ trigger: 'contract_update_take_profit',
+ },
+ contract_update_take_profit: {
+ rules: [
+ [
+ 'req',
+ {
+ condition: (contract_store: TContractStore) => !contract_store.contract_update_take_profit,
+ message: localize('Please enter a take profit amount.'),
+ },
+ ],
+ [
+ 'custom',
+ {
+ func: (value: string | number, options: TOptions, contract_store: TContractStore) => {
+ const profit = getTotalProfit(contract_store.contract_info);
+ return !(profit > 0 && +value < profit);
+ },
+ message: localize(
+ "Please enter a take profit amount that's higher than the current potential profit."
+ ),
+ },
+ ],
+ ],
+ },
+});
diff --git a/src/components/shared/utils/hooks/index.ts b/src/components/shared/utils/hooks/index.ts
new file mode 100644
index 00000000..c25cda63
--- /dev/null
+++ b/src/components/shared/utils/hooks/index.ts
@@ -0,0 +1,2 @@
+export * from './mounted';
+export * from './new-row-transition';
diff --git a/src/components/shared/utils/hooks/mounted.tsx b/src/components/shared/utils/hooks/mounted.tsx
new file mode 100644
index 00000000..5dac3377
--- /dev/null
+++ b/src/components/shared/utils/hooks/mounted.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+export const useIsMounted = () => {
+ const is_mounted = React.useRef(false);
+
+ React.useEffect(() => {
+ is_mounted.current = true;
+
+ return () => {
+ is_mounted.current = false;
+ };
+ }, []);
+ return () => is_mounted.current;
+};
diff --git a/src/components/shared/utils/hooks/new-row-transition.tsx b/src/components/shared/utils/hooks/new-row-transition.tsx
new file mode 100644
index 00000000..eef3fed1
--- /dev/null
+++ b/src/components/shared/utils/hooks/new-row-transition.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export const useNewRowTransition = (is_new_row: boolean) => {
+ const [in_prop, setInProp] = React.useState(!is_new_row);
+
+ React.useEffect(() => {
+ if (is_new_row) setInProp(true);
+ }, [is_new_row]);
+
+ return { in_prop };
+};
diff --git a/src/components/shared/utils/loader-handler/index.ts b/src/components/shared/utils/loader-handler/index.ts
new file mode 100644
index 00000000..a3ba61b4
--- /dev/null
+++ b/src/components/shared/utils/loader-handler/index.ts
@@ -0,0 +1 @@
+export * from './loader-handler';
diff --git a/src/components/shared/utils/loader-handler/loader-handler.ts b/src/components/shared/utils/loader-handler/loader-handler.ts
new file mode 100644
index 00000000..3e5983fa
--- /dev/null
+++ b/src/components/shared/utils/loader-handler/loader-handler.ts
@@ -0,0 +1,16 @@
+export const moduleLoader = (lazyComponent: () => Promise, attempts = 2, interval = 1500) => {
+ return new Promise((resolve, reject) => {
+ lazyComponent()
+ .then(resolve)
+ .catch((error: unknown) => {
+ // let us retry after 1500 ms
+ setTimeout(() => {
+ if (attempts === 1) {
+ reject(error);
+ return;
+ }
+ moduleLoader(lazyComponent, attempts - 1, interval).then(resolve, reject);
+ }, interval);
+ });
+ });
+};
diff --git a/src/components/shared/utils/loader/index.ts b/src/components/shared/utils/loader/index.ts
new file mode 100644
index 00000000..7d98c974
--- /dev/null
+++ b/src/components/shared/utils/loader/index.ts
@@ -0,0 +1 @@
+export * from './lazy-load';
diff --git a/src/components/shared/utils/loader/lazy-load.tsx b/src/components/shared/utils/loader/lazy-load.tsx
new file mode 100644
index 00000000..d26ae9a2
--- /dev/null
+++ b/src/components/shared/utils/loader/lazy-load.tsx
@@ -0,0 +1,16 @@
+import Loadable from 'react-loadable';
+
+export const makeLazyLoader =
+ (importFn: () => Promise, loaderFn: () => JSX.Element) => (component_name?: string) =>
+ Loadable.Map({
+ loader: {
+ ComponentModule: importFn,
+ },
+ render(loaded: { [key: string]: any }, props: object) {
+ const ComponentLazy = component_name
+ ? loaded.ComponentModule.default[component_name]
+ : loaded.ComponentModule.default;
+ return ;
+ },
+ loading: loaderFn,
+ });
diff --git a/src/components/shared/utils/location/index.ts b/src/components/shared/utils/location/index.ts
new file mode 100644
index 00000000..78b1d31f
--- /dev/null
+++ b/src/components/shared/utils/location/index.ts
@@ -0,0 +1 @@
+export * from './location';
diff --git a/src/components/shared/utils/location/location.ts b/src/components/shared/utils/location/location.ts
new file mode 100644
index 00000000..e54751b0
--- /dev/null
+++ b/src/components/shared/utils/location/location.ts
@@ -0,0 +1,46 @@
+import { StatesList } from '@deriv/api-types';
+
+export const getLocation = (location_list: StatesList, value: string, type: keyof StatesList[number]) => {
+ if (!value || !location_list.length) return '';
+ const location_obj = location_list.find(
+ location => location[type === 'text' ? 'value' : 'text']?.toLowerCase() === value.toLowerCase()
+ );
+
+ return location_obj?.[type] ?? '';
+};
+
+// eu countries to support
+const eu_countries = [
+ 'it',
+ 'de',
+ 'fr',
+ 'lu',
+ 'gr',
+ 'mf',
+ 'es',
+ 'sk',
+ 'lt',
+ 'nl',
+ 'at',
+ 'bg',
+ 'si',
+ 'cy',
+ 'be',
+ 'ro',
+ 'hr',
+ 'pt',
+ 'pl',
+ 'lv',
+ 'ee',
+ 'cz',
+ 'fi',
+ 'hu',
+ 'dk',
+ 'se',
+ 'ie',
+ 'im',
+ 'gb',
+ 'mt',
+];
+// check if client is from EU
+export const isEuCountry = (country: string) => eu_countries.includes(country);
diff --git a/src/components/shared/utils/login/index.ts b/src/components/shared/utils/login/index.ts
new file mode 100644
index 00000000..6cc1e6e2
--- /dev/null
+++ b/src/components/shared/utils/login/index.ts
@@ -0,0 +1 @@
+export * from './login';
diff --git a/src/components/shared/utils/login/login.ts b/src/components/shared/utils/login/login.ts
new file mode 100644
index 00000000..ac4c4653
--- /dev/null
+++ b/src/components/shared/utils/login/login.ts
@@ -0,0 +1,50 @@
+import { website_name } from '../config/app-config';
+import { domain_app_ids, getAppId } from '../config/config';
+import { CookieStorage, isStorageSupported, LocalStore } from '../storage/storage';
+import { getStaticUrl, urlForCurrentDomain } from '../url';
+import { deriv_urls } from '../url/constants';
+
+export const redirectToLogin = (is_logged_in: boolean, language: string, has_params = true, redirect_delay = 0) => {
+ if (!is_logged_in && isStorageSupported(sessionStorage)) {
+ const l = window.location;
+ const redirect_url = has_params ? window.location.href : `${l.protocol}//${l.host}${l.pathname}`;
+ sessionStorage.setItem('redirect_url', redirect_url);
+ setTimeout(() => {
+ const new_href = loginUrl({ language });
+ window.location.href = new_href;
+ }, redirect_delay);
+ }
+};
+
+export const redirectToSignUp = () => {
+ window.open(getStaticUrl('/signup/'));
+};
+
+type TLoginUrl = {
+ language: string;
+};
+
+export const loginUrl = ({ language }: TLoginUrl) => {
+ const server_url = LocalStore.get('config.server_url');
+ const signup_device_cookie = new (CookieStorage as any)('signup_device');
+ const signup_device = signup_device_cookie.get('signup_device');
+ const date_first_contact_cookie = new (CookieStorage as any)('date_first_contact');
+ const date_first_contact = date_first_contact_cookie.get('date_first_contact');
+ const marketing_queries = `${signup_device ? `&signup_device=${signup_device}` : ''}${
+ date_first_contact ? `&date_first_contact=${date_first_contact}` : ''
+ }`;
+ const getOAuthUrl = () => {
+ return `https://oauth.${
+ deriv_urls.DERIV_HOST_NAME
+ }/oauth2/authorize?app_id=${getAppId()}&l=${language}${marketing_queries}&brand=${website_name.toLowerCase()}`;
+ };
+
+ if (server_url && /qa/.test(server_url)) {
+ return `https://${server_url}/oauth2/authorize?app_id=${getAppId()}&l=${language}${marketing_queries}&brand=${website_name.toLowerCase()}`;
+ }
+
+ if (getAppId() === domain_app_ids[window.location.hostname as keyof typeof domain_app_ids]) {
+ return getOAuthUrl();
+ }
+ return urlForCurrentDomain(getOAuthUrl());
+};
diff --git a/src/components/shared/utils/number/index.ts b/src/components/shared/utils/number/index.ts
new file mode 100644
index 00000000..32448377
--- /dev/null
+++ b/src/components/shared/utils/number/index.ts
@@ -0,0 +1 @@
+export * from './number_util';
diff --git a/src/components/shared/utils/number/number_util.ts b/src/components/shared/utils/number/number_util.ts
new file mode 100644
index 00000000..ef2762bd
--- /dev/null
+++ b/src/components/shared/utils/number/number_util.ts
@@ -0,0 +1,5 @@
+export const cryptoMathRandom = () => {
+ const random_array = new Uint8Array(1);
+ const random_value = crypto.getRandomValues(random_array)[0];
+ return random_value / (2 ** 8 - 0.1);
+};
diff --git a/src/components/shared/utils/object/clone.js b/src/components/shared/utils/object/clone.js
new file mode 100644
index 00000000..5cabc08a
--- /dev/null
+++ b/src/components/shared/utils/object/clone.js
@@ -0,0 +1,267 @@
+/* eslint-disable */
+// Copied from github.com/pvorb/clone to fix an inheritance issue.
+// Line 156-158 was added to address https://github.com/pvorb/clone/issues/58
+const clone = (function () {
+ function _instanceof(obj, type) {
+ return type != null && obj instanceof type;
+ }
+
+ let nativeMap;
+ try {
+ nativeMap = Map;
+ } catch (_) {
+ // maybe a reference error because no `Map`. Give it a dummy value that no
+ // value will ever be an instanceof.
+ nativeMap = function () {};
+ }
+
+ let nativeSet;
+ try {
+ nativeSet = Set;
+ } catch (_) {
+ nativeSet = function () {};
+ }
+
+ let nativePromise;
+ try {
+ nativePromise = Promise;
+ } catch (_) {
+ nativePromise = function () {};
+ }
+
+ /**
+ * Clones (copies) an Object using deep copying.
+ *
+ * This function supports circular references by default, but if you are certain
+ * there are no circular references in your object, you can save some CPU time
+ * by calling clone(obj, false).
+ *
+ * Caution: if `circular` is false and `parent` contains circular references,
+ * your program may enter an infinite loop and crash.
+ *
+ * @param `parent` - the object to be cloned
+ * @param `circular` - set to true if the object to be cloned may contain
+ * circular references. (optional - true by default)
+ * @param `depth` - set to a number if the object is only to be cloned to
+ * a particular depth. (optional - defaults to Infinity)
+ * @param `prototype` - sets the prototype to be used when cloning an object.
+ * (optional - defaults to parent prototype).
+ * @param `includeNonEnumerable` - set to true if the non-enumerable properties
+ * should be cloned as well. Non-enumerable properties on the prototype
+ * chain will be ignored. (optional - false by default)
+ */
+ function clone(parent, circular, depth, prototype, includeNonEnumerable) {
+ if (typeof circular === 'object') {
+ depth = circular.depth;
+ prototype = circular.prototype;
+ includeNonEnumerable = circular.includeNonEnumerable;
+ circular = circular.circular;
+ }
+ // maintain two arrays for circular references, where corresponding parents
+ // and children have the same index
+ const allParents = [];
+ const allChildren = [];
+
+ const useBuffer = typeof Buffer !== 'undefined';
+
+ if (typeof circular === 'undefined') circular = true;
+
+ if (typeof depth === 'undefined') depth = Infinity;
+
+ // recurse this function so we don't reset allParents and allChildren
+ function _clone(parent, depth) {
+ // cloning null always returns null
+ if (parent === null) return null;
+
+ if (depth === 0) return parent;
+
+ let child, proto;
+ if (typeof parent !== 'object') {
+ return parent;
+ }
+
+ if (_instanceof(parent, nativeMap)) {
+ child = new nativeMap();
+ } else if (_instanceof(parent, nativeSet)) {
+ child = new nativeSet();
+ } else if (_instanceof(parent, nativePromise)) {
+ child = new nativePromise(function (resolve, reject) {
+ parent.then(
+ function (value) {
+ resolve(_clone(value, depth - 1));
+ },
+ function (err) {
+ reject(_clone(err, depth - 1));
+ }
+ );
+ });
+ } else if (clone.__isArray(parent)) {
+ child = [];
+ } else if (clone.__isRegExp(parent)) {
+ child = new RegExp(parent.source, __getRegExpFlags(parent));
+ if (parent.lastIndex) child.lastIndex = parent.lastIndex;
+ } else if (clone.__isDate(parent)) {
+ child = new Date(parent.getTime());
+ } else if (useBuffer && Buffer.isBuffer(parent)) {
+ if (Buffer.from) {
+ // Node.js >= 5.10.0
+ child = Buffer.from(parent);
+ } else {
+ // Older Node.js versions
+ child = new Buffer(parent.length);
+ parent.copy(child);
+ }
+ return child;
+ } else if (_instanceof(parent, Error)) {
+ child = Object.create(parent);
+ } else if (typeof prototype === 'undefined') {
+ proto = Object.getPrototypeOf(parent);
+ child = Object.create(proto);
+ } else {
+ child = Object.create(prototype);
+ proto = prototype;
+ }
+
+ if (circular) {
+ const index = allParents.indexOf(parent);
+
+ if (index != -1) {
+ return allChildren[index];
+ }
+ allParents.push(parent);
+ allChildren.push(child);
+ }
+
+ if (_instanceof(parent, nativeMap)) {
+ parent.forEach(function (value, key) {
+ const keyChild = _clone(key, depth - 1);
+ const valueChild = _clone(value, depth - 1);
+ child.set(keyChild, valueChild);
+ });
+ }
+ if (_instanceof(parent, nativeSet)) {
+ parent.forEach(function (value) {
+ const entryChild = _clone(value, depth - 1);
+ child.add(entryChild);
+ });
+ }
+
+ for (var i in parent) {
+ const attrs = Object.getOwnPropertyDescriptor(parent, i);
+ if (attrs) {
+ // https://github.com/pvorb/clone/issues/58
+ if (Object.keys(parent).indexOf(i) < 0) {
+ continue;
+ }
+
+ child[i] = _clone(parent[i], depth - 1);
+ }
+
+ try {
+ const objProperty = Object.getOwnPropertyDescriptor(parent, i);
+ if (objProperty.set === 'undefined') {
+ // no setter defined. Skip cloning this property
+ continue;
+ }
+ child[i] = _clone(parent[i], depth - 1);
+ } catch (e) {
+ if (e instanceof TypeError) {
+ // when in strict mode, TypeError will be thrown if child[i] property only has a getter
+ // we can't do anything about this, other than inform the user that this property cannot be set.
+ continue;
+ } else if (e instanceof ReferenceError) {
+ // this may happen in non strict mode
+ continue;
+ }
+ }
+ }
+
+ if (Object.getOwnPropertySymbols) {
+ const symbols = Object.getOwnPropertySymbols(parent);
+ for (var i = 0; i < symbols.length; i++) {
+ // Don't need to worry about cloning a symbol because it is a primitive,
+ // like a number or string.
+ const symbol = symbols[i];
+ var descriptor = Object.getOwnPropertyDescriptor(parent, symbol);
+ if (descriptor && !descriptor.enumerable && !includeNonEnumerable) {
+ continue;
+ }
+ child[symbol] = _clone(parent[symbol], depth - 1);
+ Object.defineProperty(child, symbol, descriptor);
+ }
+ }
+
+ if (includeNonEnumerable) {
+ const allPropertyNames = Object.getOwnPropertyNames(parent);
+ for (var i = 0; i < allPropertyNames.length; i++) {
+ const propertyName = allPropertyNames[i];
+ var descriptor = Object.getOwnPropertyDescriptor(parent, propertyName);
+ if (descriptor && descriptor.enumerable) {
+ continue;
+ }
+ child[propertyName] = _clone(parent[propertyName], depth - 1);
+ Object.defineProperty(child, propertyName, descriptor);
+ }
+ }
+
+ return child;
+ }
+
+ return _clone(parent, depth);
+ }
+
+ /**
+ * Simple flat clone using prototype, accepts only objects, usefull for property
+ * override on FLAT configuration object (no nested props).
+ *
+ * USE WITH CAUTION! This may not behave as you wish if you do not know how this
+ * works.
+ */
+ clone.clonePrototype = function clonePrototype(parent) {
+ if (parent === null) return null;
+
+ const c = function () {};
+ c.prototype = parent;
+ return new c();
+ };
+
+ // private utility functions
+
+ function __objToStr(o) {
+ return Object.prototype.toString.call(o);
+ }
+ clone.__objToStr = __objToStr;
+
+ function __isDate(o) {
+ return typeof o === 'object' && __objToStr(o) === '[object Date]';
+ }
+ clone.__isDate = __isDate;
+
+ function __isArray(o) {
+ return typeof o === 'object' && __objToStr(o) === '[object Array]';
+ }
+ clone.__isArray = __isArray;
+
+ function __isRegExp(o) {
+ return typeof o === 'object' && __objToStr(o) === '[object RegExp]';
+ }
+ clone.__isRegExp = __isRegExp;
+
+ function __getRegExpFlags(re) {
+ let flags = '';
+ if (re.global) flags += 'g';
+ if (re.ignoreCase) flags += 'i';
+ if (re.multiline) flags += 'm';
+ return flags;
+ }
+ clone.__getRegExpFlags = __getRegExpFlags;
+
+ return clone;
+})();
+
+if (typeof module === 'object' && module.exports) {
+ module.exports = clone;
+}
+
+export default clone;
+/* eslint-enable */
diff --git a/src/components/shared/utils/object/index.ts b/src/components/shared/utils/object/index.ts
new file mode 100644
index 00000000..ff6b5fad
--- /dev/null
+++ b/src/components/shared/utils/object/index.ts
@@ -0,0 +1,2 @@
+export * from './object';
+export { default as cloneThorough } from './clone';
diff --git a/src/components/shared/utils/object/object.ts b/src/components/shared/utils/object/object.ts
new file mode 100644
index 00000000..4f8b4d76
--- /dev/null
+++ b/src/components/shared/utils/object/object.ts
@@ -0,0 +1,135 @@
+/* eslint-disable */
+
+export const removeObjProperties = (property_arr: string[], { ...obj }) => {
+ property_arr.forEach(property => delete obj[property]);
+ return obj;
+};
+
+export const filterObjProperties = ({ ...obj }, property_arr: string[]) =>
+ Object.fromEntries(
+ Object.entries(obj)
+ // eslint-disable-next-line no-unused-vars
+ .filter(([key, _]) => property_arr.includes(key))
+ );
+
+export const isEmptyObject = (obj: any) => {
+ let is_empty = true;
+ if (obj && obj instanceof Object) {
+ Object.keys(obj).forEach(key => {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) is_empty = false;
+ });
+ }
+ return is_empty;
+};
+
+export const cloneObject = (obj: any) => {
+ if (isEmptyObject(obj)) {
+ return obj;
+ }
+
+ const result = Array.isArray(obj) ? [] : {};
+
+ // Loop through enumerable own properties
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ result[key] = obj[key];
+ }
+ }
+
+ return result;
+};
+
+// Note that this function breaks on objects with circular references.
+export const isDeepEqual = (a: any, b: any) => {
+ if (typeof a !== typeof b) {
+ return false;
+ } else if (Array.isArray(a)) {
+ return isEqualArray(a, b);
+ } else if (a && b && typeof a === 'object') {
+ return isEqualObject(a, b);
+ } else if (typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b)) {
+ return true;
+ }
+ // else
+ return a === b;
+};
+
+export const isEqualArray = (arr1: any[], arr2: any[]): boolean =>
+ arr1 === arr2 || (arr1.length === arr2.length && arr1.every((value, idx) => isDeepEqual(value, arr2[idx])));
+
+export const isEqualObject = (obj1: any, obj2: any): boolean =>
+ obj1 === obj2 ||
+ (Object.keys(obj1).length === Object.keys(obj2).length &&
+ Object.keys(obj1).every(key => isDeepEqual(obj1[key], obj2[key])));
+
+// Filters out duplicates in an array of objects by key
+export const unique = (array: any[], key: string) =>
+ array.filter((e, idx) => array.findIndex((a, i) => (a[key] ? a[key] === e[key] : i === idx)) === idx);
+
+export const getPropertyValue = (obj: any, k: string | string[]): any => {
+ let keys = k;
+ if (!Array.isArray(keys)) keys = [keys];
+ if (!isEmptyObject(obj) && keys[0] in obj && keys && keys.length > 1) {
+ return getPropertyValue(obj[keys[0]], keys.slice(1));
+ }
+ // else return clone of object to avoid overwriting data
+ return obj ? cloneObject(obj[keys[0]]) : undefined;
+};
+
+export const removeEmptyPropertiesFromObject = (obj: any) => {
+ const clone = { ...obj };
+
+ Object.getOwnPropertyNames(obj).forEach(key => {
+ if ([undefined, null, ''].includes(obj[key])) {
+ delete clone[key];
+ }
+ });
+
+ return clone;
+};
+
+export const sequence = (n: number) => Array.from(Array(n).keys());
+
+export const pick = (source: any, fields: any) => {
+ return fields.reduce((target: any, prop: any) => {
+ if (Object.prototype.hasOwnProperty.call(source, prop)) target[prop] = source[prop];
+ return target;
+ }, {});
+};
+
+export const findValueByKeyRecursively = (obj: any, key: string) => {
+ let return_value;
+
+ Object.keys(obj).some(obj_key => {
+ const value = obj[obj_key];
+
+ if (obj_key === key) {
+ return_value = obj[key];
+ return true;
+ }
+
+ if (typeof value === 'object') {
+ const nested_value = findValueByKeyRecursively(value, key);
+
+ if (nested_value) {
+ return_value = nested_value;
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ return return_value;
+};
+
+// Recursively freeze an object (deep freeze)
+export const deepFreeze = (obj: any) => {
+ Object.getOwnPropertyNames(obj).forEach(key => {
+ const value = obj[key];
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
+ deepFreeze(value);
+ }
+ });
+ return Object.freeze(obj);
+};
diff --git a/src/components/shared/utils/os/index.ts b/src/components/shared/utils/os/index.ts
new file mode 100644
index 00000000..cba793d4
--- /dev/null
+++ b/src/components/shared/utils/os/index.ts
@@ -0,0 +1 @@
+export * from './os_detect';
diff --git a/src/components/shared/utils/os/os_detect.ts b/src/components/shared/utils/os/os_detect.ts
new file mode 100644
index 00000000..273a4a65
--- /dev/null
+++ b/src/components/shared/utils/os/os_detect.ts
@@ -0,0 +1,263 @@
+import UAParser from 'ua-parser-js';
+
+declare global {
+ interface Window {
+ opera?: string;
+ MSStream?: {
+ readonly type: string;
+ msClose: () => void;
+ msDetachStream: () => void;
+ };
+ }
+ interface Navigator {
+ userAgentData?: NavigatorUAData;
+ }
+}
+
+type NavigatorUAData = {
+ brands: Array<{ brand: string; version: string }>;
+ mobile: boolean;
+ getHighEntropyValues(hints: string[]): Promise;
+};
+
+type HighEntropyValues = {
+ platform?: string;
+ platformVersion?: string;
+ model?: string;
+ uaFullVersion?: string;
+};
+
+export const systems = {
+ mac: ['Mac68K', 'MacIntel', 'MacPPC'],
+ linux: [
+ 'HP-UX',
+ 'Linux i686',
+ 'Linux amd64',
+ 'Linux i686 on x86_64',
+ 'Linux i686 X11',
+ 'Linux x86_64',
+ 'Linux x86_64 X11',
+ 'FreeBSD',
+ 'FreeBSD i386',
+ 'FreeBSD amd64',
+ 'X11',
+ ],
+ ios: ['iPhone', 'iPod', 'iPad', 'iPhone Simulator', 'iPod Simulator', 'iPad Simulator'],
+ android: [
+ 'Android',
+ 'Linux armv7l', // Samsung galaxy s2 ~ s5, nexus 4/5
+ 'Linux armv8l',
+ null,
+ ],
+ windows: ['Win16', 'Win32', 'Win64', 'WinCE'],
+};
+
+export const isDesktopOs = () => {
+ const os = OSDetect();
+ return !!['windows', 'mac', 'linux'].find(system => system === os);
+};
+
+export const isMobileOs = () =>
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
+
+export const OSDetect = () => {
+ // For testing purposes or more compatibility, if we set 'config.os'
+ // inside our localStorage, we ignore fetching information from
+ // navigator object and return what we have straight away.
+ if (localStorage.getItem('config.os')) {
+ return localStorage.getItem('config.os');
+ }
+ if (typeof navigator !== 'undefined' && navigator.platform) {
+ return Object.keys(systems)
+ .map(os => {
+ if (systems[os as keyof typeof systems].some(platform => navigator.platform === platform)) {
+ return os;
+ }
+ return false;
+ })
+ .filter(os => os)[0];
+ }
+
+ return 'Unknown OS';
+};
+
+export const mobileOSDetect = () => {
+ const userAgent = navigator.userAgent || navigator.vendor || window.opera || '';
+ // huawei devices regex from: https://gist.github.com/megaacheyounes/e1c7eec5c790e577db602381b8c50bfa
+ const huaweiDevicesRegex =
+ /\bK\b|ALP-|AMN-|ANA-|ANE-|ANG-|AQM-|ARS-|ART-|ATU-|BAC-|BLA-|BRQ-|CAG-|CAM-|CAN-|CAZ-|CDL-|CDY-|CLT-|CRO-|CUN-|DIG-|DRA-|DUA-|DUB-|DVC-|ELE-|ELS-|EML-|EVA-|EVR-|FIG-|FLA-|FRL-|GLK-|HMA-|HW-|HWI-|INE-|JAT-|JEF-|JER-|JKM-|JNY-|JSC-|LDN-|LIO-|LON-|LUA-|LYA-|LYO-|MAR-|MED-|MHA-|MLA-|MRD-|MYA-|NCE-|NEO-|NOH-|NOP-|OCE-|PAR-|PIC-|POT-|PPA-|PRA-|RNE-|SEA-|SLA-|SNE-|SPN-|STK-|TAH-|TAS-|TET-|TRT-|VCE-|VIE-|VKY-|VNS-|VOG-|VTR-|WAS-|WKG-|WLZ-|JAD-|WKG-|MLD-|RTE-|NAM-|NEN-|BAL-|JAD-|JLN-|YAL/i;
+
+ // Windows Phone must come first because its UA also contains "Android"
+ if (/windows phone/i.test(userAgent)) {
+ return 'Windows Phone';
+ }
+
+ if (/android/i.test(userAgent)) {
+ // Huawei UA is the same as android so we have to detect by the model
+ if (huaweiDevicesRegex.test(userAgent) || /huawei/i.test(userAgent)) {
+ return 'huawei';
+ }
+ return 'Android';
+ }
+
+ // iOS detection from: http://stackoverflow.com/a/9039885/177710
+ if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
+ return 'iOS';
+ }
+
+ return 'unknown';
+};
+
+// Simple regular expression to match potential Huawei device codes
+const huaweiDevicesRegex = /\b([A-Z]{3}-)\b/gi;
+
+// Set of valid Huawei device codes
+const validCodes = new Set([
+ 'ALP-',
+ 'AMN-',
+ 'ANA-',
+ 'ANE-',
+ 'ANG-',
+ 'AQM-',
+ 'ARS-',
+ 'ART-',
+ 'ATU-',
+ 'BAC-',
+ 'BLA-',
+ 'BRQ-',
+ 'CAG-',
+ 'CAM-',
+ 'CAN-',
+ 'CAZ-',
+ 'CDL-',
+ 'CDY-',
+ 'CLT-',
+ 'CRO-',
+ 'CUN-',
+ 'DIG-',
+ 'DRA-',
+ 'DUA-',
+ 'DUB-',
+ 'DVC-',
+ 'ELE-',
+ 'ELS-',
+ 'EML-',
+ 'EVA-',
+ 'EVR-',
+ 'FIG-',
+ 'FLA-',
+ 'FRL-',
+ 'GLK-',
+ 'HMA-',
+ 'HW-',
+ 'HWI-',
+ 'INE-',
+ 'JAT-',
+ 'JEF-',
+ 'JER-',
+ 'JKM-',
+ 'JNY-',
+ 'JSC-',
+ 'LDN-',
+ 'LIO-',
+ 'LON-',
+ 'LUA-',
+ 'LYA-',
+ 'LYO-',
+ 'MAR-',
+ 'MED-',
+ 'MHA-',
+ 'MLA-',
+ 'MRD-',
+ 'MYA-',
+ 'NCE-',
+ 'NEO-',
+ 'NOH-',
+ 'NOP-',
+ 'OCE-',
+ 'PAR-',
+ 'PIC-',
+ 'POT-',
+ 'PPA-',
+ 'PRA-',
+ 'RNE-',
+ 'SEA-',
+ 'SLA-',
+ 'SNE-',
+ 'SPN-',
+ 'STK-',
+ 'TAH-',
+ 'TAS-',
+ 'TET-',
+ 'TRT-',
+ 'VCE-',
+ 'VIE-',
+ 'VKY-',
+ 'VNS-',
+ 'VOG-',
+ 'VTR-',
+ 'WAS-',
+ 'WKG-',
+ 'WLZ-',
+ 'JAD-',
+ 'MLD-',
+ 'RTE-',
+ 'NAM-',
+ 'NEN-',
+ 'BAL-',
+ 'JLN-',
+ 'YAL-',
+ 'MGA-',
+ 'FGD-',
+ 'XYAO-',
+ 'BON-',
+ 'ALN-',
+ 'ALT-',
+ 'BRA-',
+ 'DBY2-',
+ 'STG-',
+ 'MAO-',
+ 'LEM-',
+ 'GOA-',
+ 'FOA-',
+ 'MNA-',
+ 'LNA-',
+]);
+
+// Function to validate Huawei device codes from a string
+function validateHuaweiCodes(inputString: string) {
+ const matches = inputString.match(huaweiDevicesRegex);
+ if (matches) {
+ return matches.filter(code => validCodes.has(code.toUpperCase())).length > 0;
+ }
+ return false;
+}
+
+export const mobileOSDetectAsync = async () => {
+ const userAgent = navigator.userAgent ?? window.opera ?? '';
+ // Windows Phone must come first because its UA also contains "Android"
+ if (/windows phone/i.test(userAgent)) {
+ return 'Windows Phone';
+ }
+
+ if (/android/i.test(userAgent)) {
+ // Check if navigator.userAgentData is available for modern browsers
+ if (navigator?.userAgentData) {
+ const ua = await navigator.userAgentData.getHighEntropyValues(['model']);
+ if (validateHuaweiCodes(ua?.model || '')) {
+ return 'huawei';
+ }
+ } else if (validateHuaweiCodes(userAgent) || /huawei/i.test(userAgent)) {
+ return 'huawei';
+ }
+ return 'Android';
+ }
+
+ if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
+ return 'iOS';
+ }
+
+ return 'unknown';
+};
+
+export const getOSNameWithUAParser = () => UAParser().os.name;
diff --git a/src/components/shared/utils/platform/index.ts b/src/components/shared/utils/platform/index.ts
new file mode 100644
index 00000000..1841b995
--- /dev/null
+++ b/src/components/shared/utils/platform/index.ts
@@ -0,0 +1 @@
+export * from './platform';
diff --git a/src/components/shared/utils/platform/platform.ts b/src/components/shared/utils/platform/platform.ts
new file mode 100644
index 00000000..5e761a85
--- /dev/null
+++ b/src/components/shared/utils/platform/platform.ts
@@ -0,0 +1,166 @@
+import { getPlatformSettings } from '../brand';
+import { routes } from '../routes';
+
+type TRoutingHistory = {
+ action: string;
+ hash: string;
+ key: string;
+ pathname: string;
+ search: string;
+}[];
+
+/*
+ * These functions exist because we want to refresh the browser page on switch between Bot and the rest of the platforms.
+ * */
+
+export const platform_name = Object.freeze({
+ DBot: getPlatformSettings('dbot').name,
+ DTrader: getPlatformSettings('trader').name,
+ DXtrade: getPlatformSettings('dxtrade').name,
+ DMT5: getPlatformSettings('mt5').name,
+ SmartTrader: getPlatformSettings('smarttrader').name,
+ BinaryBot: getPlatformSettings('bbot').name,
+ DerivGO: getPlatformSettings('go').name,
+});
+
+export const CFD_PLATFORMS = Object.freeze({
+ MT5: 'mt5',
+ DXTRADE: 'dxtrade',
+ CTRADER: 'ctrader',
+});
+
+export const isBot = () =>
+ /^\/bot/.test(window.location.pathname) ||
+ (/^\/(br_)/.test(window.location.pathname) && window.location.pathname.split('/')[2] === 'bot');
+
+export const isMT5 = () =>
+ /^\/mt5/.test(window.location.pathname) ||
+ (/^\/(br_)/.test(window.location.pathname) && window.location.pathname.split('/')[2] === CFD_PLATFORMS.MT5);
+
+export const isDXtrade = () =>
+ /^\/derivx/.test(window.location.pathname) ||
+ (/^\/(br_)/.test(window.location.pathname) && window.location.pathname.split('/')[2] === 'derivx');
+
+export const isNavigationFromDerivGO = () => window.sessionStorage.getItem('config.platform') === 'derivgo';
+
+export const isNavigationFromP2P = () => window.sessionStorage.getItem('config.platform') === 'dp2p';
+
+export const getPathname = () => {
+ if (isBot()) return platform_name.DBot;
+ if (isMT5()) return platform_name.DMT5;
+ if (isDXtrade()) return platform_name.DXtrade;
+ switch (window.location.pathname.split('/')[1]) {
+ case '':
+ return platform_name.DTrader;
+ case 'reports':
+ return 'Reports';
+ case 'cashier':
+ return 'Cashier';
+ default:
+ return platform_name.SmartTrader;
+ }
+};
+
+export const getPlatformInformation = (routing_history: TRoutingHistory) => {
+ if (isBot() || isNavigationFromPlatform(routing_history, routes.bot)) {
+ return { header: platform_name.DBot, icon: getPlatformSettings('dbot').icon };
+ }
+
+ if (isMT5() || isNavigationFromPlatform(routing_history, routes.mt5)) {
+ return { header: platform_name.DMT5, icon: getPlatformSettings('mt5').icon };
+ }
+
+ if (isDXtrade() || isNavigationFromPlatform(routing_history, routes.dxtrade)) {
+ return { header: platform_name.DXtrade, icon: getPlatformSettings('dxtrade').icon };
+ }
+
+ if (isNavigationFromExternalPlatform(routing_history, routes.smarttrader)) {
+ return { header: platform_name.SmartTrader, icon: getPlatformSettings('smarttrader').icon };
+ }
+
+ return { header: platform_name.DTrader, icon: getPlatformSettings('trader').icon };
+};
+
+export const getActivePlatform = (routing_history: TRoutingHistory) => {
+ if (isBot() || isNavigationFromPlatform(routing_history, routes.bot)) return platform_name.DBot;
+ if (isMT5() || isNavigationFromPlatform(routing_history, routes.mt5)) return platform_name.DMT5;
+ if (isDXtrade() || isNavigationFromPlatform(routing_history, routes.dxtrade)) return platform_name.DXtrade;
+ if (isNavigationFromExternalPlatform(routing_history, routes.smarttrader)) return platform_name.SmartTrader;
+ if (isNavigationFromExternalPlatform(routing_history, routes.binarybot)) return platform_name.BinaryBot;
+ return platform_name.DTrader;
+};
+
+export const getPlatformRedirect = (routing_history: TRoutingHistory) => {
+ if (isBot() || isNavigationFromPlatform(routing_history, routes.bot))
+ return { name: platform_name.DBot, route: routes.bot };
+ if (isMT5() || isNavigationFromPlatform(routing_history, routes.mt5))
+ return { name: platform_name.DMT5, route: routes.mt5 };
+ if (isDXtrade() || isNavigationFromPlatform(routing_history, routes.dxtrade))
+ return { name: platform_name.DXtrade, route: routes.dxtrade };
+ if (isNavigationFromExternalPlatform(routing_history, routes.smarttrader))
+ return { name: platform_name.SmartTrader, route: routes.smarttrader };
+ if (isNavigationFromP2P()) return { name: 'P2P', route: routes.cashier_p2p, ref: 'p2p' };
+ if (isNavigationFromExternalPlatform(routing_history, routes.binarybot))
+ return { name: platform_name.BinaryBot, route: routes.binarybot };
+ return { name: platform_name.DTrader, route: routes.trade };
+};
+
+export const isNavigationFromPlatform = (
+ app_routing_history: TRoutingHistory,
+ platform_route: string,
+ should_ignore_parent_path = false
+) => {
+ if (app_routing_history.length > 0) {
+ const getParentPath = (pathname: string) => (/^http/.test(pathname) ? false : pathname.split('/')[1]);
+
+ for (let i = 0; i < app_routing_history.length; i++) {
+ const history_item = app_routing_history[i];
+ const history_item_parent_path = getParentPath(history_item.pathname);
+ const next_history_item = app_routing_history.length > i + 1 && app_routing_history[i + 1];
+
+ if (
+ history_item_parent_path === getParentPath(platform_route) ||
+ (should_ignore_parent_path && history_item.pathname === platform_route)
+ ) {
+ return true;
+ } else if (!next_history_item) {
+ return false;
+ } else if (history_item_parent_path === getParentPath(next_history_item.pathname)) {
+ // Continue walking until we see passed in platform_route.
+ continue; // eslint-disable-line no-continue
+ } else {
+ // Return false when path matches a platform parent path, but don't return anything
+ // when a non-platform path was seen. i.e. navigating between /cashier and /reports
+ // should not affect navigating back to platform when clicking cross.
+ const platform_parent_paths = [routes.mt5, routes.dxtrade, routes.bot, routes.trade].map(route =>
+ getParentPath(route)
+ );
+ const is_other_platform_path = platform_parent_paths.includes(history_item_parent_path);
+
+ if (is_other_platform_path) {
+ break;
+ }
+ }
+ }
+ }
+
+ return false;
+};
+
+export const isNavigationFromExternalPlatform = (routing_history: TRoutingHistory, platform_route: string) => {
+ /*
+ * Check if the client is navigating from external platform(SmartTrader or BinaryBot)
+ * and has not visited Dtrader after it.
+ */
+
+ const platform_index = routing_history.findIndex(history_item => history_item.pathname === platform_route);
+ const dtrader_index = routing_history.findIndex(history_item => history_item.pathname === routes.trade);
+ const has_visited_platform = platform_index !== -1;
+ const has_visited_dtrader = dtrader_index !== -1;
+
+ if (has_visited_platform) {
+ return has_visited_dtrader ? platform_index < dtrader_index : true;
+ }
+
+ return false;
+};
diff --git a/src/components/shared/utils/promise/index.ts b/src/components/shared/utils/promise/index.ts
new file mode 100644
index 00000000..3fd8faaf
--- /dev/null
+++ b/src/components/shared/utils/promise/index.ts
@@ -0,0 +1 @@
+export * from './make-cancellable-promise';
diff --git a/src/components/shared/utils/promise/make-cancellable-promise.ts b/src/components/shared/utils/promise/make-cancellable-promise.ts
new file mode 100644
index 00000000..159b4429
--- /dev/null
+++ b/src/components/shared/utils/promise/make-cancellable-promise.ts
@@ -0,0 +1,27 @@
+export const makeCancellablePromise = (listener: Promise) => {
+ let done = false;
+ let cancel: () => boolean | void = () => (done = true);
+ const promise = new Promise((resolve, reject) => {
+ cancel = () => {
+ // If it is already done, don't do anything.
+ if (!done) {
+ done = true;
+ reject(new Error('Cancelled'));
+ }
+ };
+ listener
+ .then(result => {
+ if (done) {
+ // Promise is canceled or done.
+ reject(result);
+ }
+ done = true;
+ resolve(result);
+ })
+ .catch((error: Error) => {
+ done = true;
+ reject(error);
+ });
+ });
+ return { promise, cancel };
+};
diff --git a/src/components/shared/utils/route/index.ts b/src/components/shared/utils/route/index.ts
new file mode 100644
index 00000000..d9922192
--- /dev/null
+++ b/src/components/shared/utils/route/index.ts
@@ -0,0 +1 @@
+export * from './route';
diff --git a/src/components/shared/utils/route/route.tsx b/src/components/shared/utils/route/route.tsx
new file mode 100644
index 00000000..67ff6fb1
--- /dev/null
+++ b/src/components/shared/utils/route/route.tsx
@@ -0,0 +1,41 @@
+// Checks if pathname matches route. (Works even with query string /?)
+
+// TODO: Add test cases for this
+import React from 'react';
+import { Redirect } from 'react-router-dom';
+
+export type TRoute = Partial<{
+ component: React.ElementType | null | ((routes?: TRoute[]) => JSX.Element) | typeof Redirect;
+ default: boolean;
+ exact: boolean;
+ getTitle: () => string;
+ icon_component: string;
+ id: string;
+ is_authenticated: boolean;
+ is_invisible: boolean;
+ path: string;
+ to: string;
+ icon: string;
+ is_disabled: boolean;
+ subroutes: TRoute[];
+ routes: TRoute[];
+}>;
+
+type TGetSelectedRoute = {
+ routes: TRoute[];
+ pathname: string;
+};
+
+// @ts-expect-error as this is a utility function with dynamic types
+export const matchRoute = (route: T, pathname: string) => new RegExp(`^${route?.path}(/.*)?$`).test(pathname);
+
+export const getSelectedRoute = ({ routes, pathname }: TGetSelectedRoute) => {
+ const matching_route = routes.find(route => matchRoute(route, pathname));
+ if (!matching_route) {
+ return routes.find(route => route.default) || routes[0] || null;
+ }
+ return matching_route;
+};
+
+export const isRouteVisible = (route: TRoute, is_logged_in: boolean) =>
+ !(route && route.is_authenticated && !is_logged_in);
diff --git a/src/components/shared/utils/routes/index.ts b/src/components/shared/utils/routes/index.ts
new file mode 100644
index 00000000..a3820983
--- /dev/null
+++ b/src/components/shared/utils/routes/index.ts
@@ -0,0 +1 @@
+export * from './routes';
diff --git a/src/components/shared/utils/routes/routes.ts b/src/components/shared/utils/routes/routes.ts
new file mode 100644
index 00000000..ef88e4ed
--- /dev/null
+++ b/src/components/shared/utils/routes/routes.ts
@@ -0,0 +1,98 @@
+import { getUrlBinaryBot, getUrlSmartTrader } from '../url/helpers';
+
+export const routes = {
+ error404: '/404',
+ account: '/account',
+ trading_assessment: '/account/trading-assessment',
+ languages: '/account/languages',
+ financial_assessment: '/account/financial-assessment',
+ personal_details: '/account/personal-details',
+ proof_of_identity: '/account/proof-of-identity',
+ proof_of_address: '/account/proof-of-address',
+ proof_of_ownership: '/account/proof-of-ownership',
+ proof_of_income: '/account/proof-of-income',
+ passwords: '/account/passwords',
+ passkeys: '/account/passkeys',
+ closing_account: '/account/closing-account',
+ deactivate_account: '/account/deactivate-account', // TODO: Remove once mobile team has changed this link
+ account_closed: '/account-closed',
+ account_limits: '/account/account-limits',
+ connected_apps: '/account/connected-apps',
+ api_token: '/account/api-token',
+ login_history: '/account/login-history',
+ two_factor_authentication: '/account/two-factor-authentication',
+ self_exclusion: '/account/self-exclusion',
+ account_password: '/settings/account_password',
+ apps: '/settings/apps',
+ cashier_password: '/settings/cashier_password',
+ contract: '/contract/:contract_id',
+ exclusion: '/settings/exclusion',
+ financial: '/settings/financial',
+ history: '/settings/history',
+ index: '/index',
+ limits: '/settings/limits',
+ mt5: '/mt5',
+ dxtrade: '/derivx',
+ personal: '/settings/personal',
+ positions: '/reports/positions',
+ profit: '/reports/profit',
+ reports: '/reports',
+ root: '/',
+ reset_password: '/',
+ redirect: '/redirect',
+ settings: '/settings',
+ statement: '/reports/statement',
+ token: '/settings/token',
+ trade: '/',
+ bot: '/bot',
+ cashier: '/cashier',
+ cashier_deposit: '/cashier/deposit',
+ cashier_withdrawal: '/cashier/withdrawal',
+ cashier_pa: '/cashier/payment-agent',
+ cashier_acc_transfer: '/cashier/account-transfer',
+ cashier_transactions_crypto: '/cashier/crypto-transactions',
+ // cashier_offramp: '/cashier/off-ramp',
+ cashier_onramp: '/cashier/on-ramp',
+ cashier_p2p: '/cashier/p2p',
+ cashier_p2p_v2: '/cashier/p2p-v2',
+
+ // P2P
+ p2p_verification: '/cashier/p2p/verification',
+ p2p_buy_sell: '/cashier/p2p/buy-sell',
+ p2p_orders: '/cashier/p2p/orders',
+ p2p_my_ads: '/cashier/p2p/my-ads',
+ p2p_my_profile: '/cashier/p2p/my-profile',
+ p2p_advertiser_page: '/cashier/p2p/advertiser',
+ p2p_v2_inner: '/cashier/p2p-v2/inner',
+
+ cashier_pa_transfer: '/cashier/payment-agent-transfer',
+ smarttrader: getUrlSmartTrader(),
+ binarybot: getUrlBinaryBot(),
+ endpoint: '/endpoint',
+ complaints_policy: '/complaints-policy',
+
+ // Appstore
+ appstore: '/appstore',
+ traders_hub: '/appstore/traders-hub',
+ onboarding: '/appstore/onboarding',
+ compare_cfds: '/appstore/cfd-compare-acccounts',
+
+ // Wallets
+ wallets: '/wallets',
+ wallets_cashier: '/wallets/cashier',
+ wallets_deposit: '/wallets/cashier/deposit',
+ wallets_withdrawal: '/wallets/cashier/withdraw',
+ wallets_transfer: 'wallets/cashier/transfer',
+ wallets_transactions: '/wallets/cashier/transactions',
+ wallets_compare_accounts: '/wallets/compare-accounts',
+
+ // Traders Hub
+ traders_hub_v2: '/traders-hub',
+ compare_accounts: '/traders-hub/compare-accounts',
+
+ // Account V2
+ account_v2: '/account-v2',
+
+ // Cashier V2
+ cashier_v2: '/cashier-v2',
+};
diff --git a/src/components/shared/utils/screen/index.ts b/src/components/shared/utils/screen/index.ts
new file mode 100644
index 00000000..df34a59e
--- /dev/null
+++ b/src/components/shared/utils/screen/index.ts
@@ -0,0 +1 @@
+export * from './responsive';
diff --git a/src/components/shared/utils/screen/responsive.ts b/src/components/shared/utils/screen/responsive.ts
new file mode 100644
index 00000000..29a3008a
--- /dev/null
+++ b/src/components/shared/utils/screen/responsive.ts
@@ -0,0 +1,24 @@
+declare global {
+ interface Navigator {
+ msMaxTouchPoints: number;
+ }
+ interface Window {
+ // TODO DocumentTouch been removed from the standards, we need to change this with Touch and TouchList later
+ DocumentTouch: any;
+ }
+}
+
+export const MAX_MOBILE_WIDTH = 926; // iPhone 12 Pro Max has the world largest viewport size of 428 x 926
+export const MAX_TABLET_WIDTH = 1081;
+
+export const isTouchDevice = () =>
+ 'ontouchstart' in window ||
+ 'ontouchstart' in document.documentElement ||
+ (window.DocumentTouch && document instanceof window.DocumentTouch) ||
+ navigator.maxTouchPoints > 0 ||
+ window.navigator.msMaxTouchPoints > 0;
+/** @deprecated Use `is_mobile` from ui-store instead. */
+export const isMobile = () => window.innerWidth <= MAX_MOBILE_WIDTH;
+export const isDesktop = () => isTablet() || window.innerWidth > MAX_TABLET_WIDTH; // TODO: remove tablet once there is a design for the specific size.
+export const isTablet = () => MAX_MOBILE_WIDTH < window.innerWidth && window.innerWidth <= MAX_TABLET_WIDTH;
+export const isTabletDrawer = () => window.innerWidth < 768;
diff --git a/src/components/shared/utils/shortcode/index.ts b/src/components/shared/utils/shortcode/index.ts
new file mode 100644
index 00000000..a9bd1df3
--- /dev/null
+++ b/src/components/shared/utils/shortcode/index.ts
@@ -0,0 +1 @@
+export * from './shortcode';
diff --git a/src/components/shared/utils/shortcode/shortcode.ts b/src/components/shared/utils/shortcode/shortcode.ts
new file mode 100644
index 00000000..98c69b24
--- /dev/null
+++ b/src/components/shared/utils/shortcode/shortcode.ts
@@ -0,0 +1,102 @@
+type TIsHighLow = {
+ shortcode?: string;
+ shortcode_info?: {
+ category?: string;
+ underlying?: string;
+ barrier_1?: string;
+ multiplier?: string;
+ start_time?: string;
+ };
+};
+
+type TInfoFromShortcode = Record<
+ | 'category'
+ | 'underlying'
+ | 'barrier_1'
+ | 'multiplier'
+ | 'start_time'
+ | 'payout_tick'
+ | 'growth_rate'
+ | 'growth_frequency',
+ string
+>;
+
+// category_underlying_amount
+const base_pattern =
+ '^([A-Z]+)_((?:1HZ[0-9-V]+)|(?:(?:CRASH|BOOM)[0-9\\d]+[A-Z]?)|(?:cry_[A-Z]+)|(?:JD[0-9]+)|(?:OTC_[A-Z0-9]+)|R_[\\d]{2,3}|[A-Z]+)_([\\d.]+)';
+
+// category_underlying_amount_payouttick_growthrate_growthfrequency_ticksizebarrier_starttime
+const accumulators_regex = new RegExp(`${base_pattern}_(\\d+)_(\\d*\\.?\\d*)_(\\d+)_(\\d*\\.?\\d*)_(\\d+)`);
+
+// category_underlying_amount_multiplier_starttime
+const multipliers_regex = new RegExp(`${base_pattern}_(\\d+)_(\\d+)`);
+
+// category_underlying_amount_starttime_endtime_barrier
+const options_regex = new RegExp(`${base_pattern}_([A-Z\\d]+)_([A-Z\\d]+)_?([A-Z\\d]+)?`);
+
+export const extractInfoFromShortcode = (shortcode: string): TInfoFromShortcode => {
+ const info_from_shortcode = {
+ category: '',
+ underlying: '',
+ barrier_1: '',
+ multiplier: '',
+ start_time: '',
+ payout_tick: '',
+ growth_rate: '',
+ growth_frequency: '',
+ };
+
+ const is_accumulators = /^ACCU/i.test(shortcode);
+ const is_multipliers = /^MULT/i.test(shortcode);
+
+ // First group of regex pattern captures the trade category, second group captures the market's underlying
+ let pattern;
+ if (is_multipliers) {
+ pattern = multipliers_regex;
+ } else pattern = is_accumulators ? accumulators_regex : options_regex;
+ const extracted = pattern.exec(shortcode);
+
+ if (extracted !== null) {
+ info_from_shortcode.category = extracted[1].charAt(0).toUpperCase() + extracted[1].slice(1).toLowerCase();
+ info_from_shortcode.underlying = extracted[2];
+
+ if (is_multipliers) {
+ info_from_shortcode.multiplier = extracted[4];
+ info_from_shortcode.start_time = extracted[5];
+ } else if (is_accumulators) {
+ info_from_shortcode.payout_tick = extracted[4];
+ info_from_shortcode.growth_rate = extracted[5];
+ info_from_shortcode.growth_frequency = extracted[6];
+ info_from_shortcode.start_time = extracted[8];
+ } else {
+ info_from_shortcode.start_time = extracted[4];
+ }
+
+ if (/^(CALL|PUT)$/i.test(info_from_shortcode.category)) {
+ info_from_shortcode.barrier_1 = extracted[6];
+ }
+ }
+
+ return info_from_shortcode;
+};
+
+export const isHighLow = ({ shortcode = '', shortcode_info }: TIsHighLow) => {
+ const info_from_shortcode = shortcode ? extractInfoFromShortcode(shortcode) : shortcode_info;
+ return info_from_shortcode && info_from_shortcode.barrier_1 ? !/^S0P$/.test(info_from_shortcode.barrier_1) : false;
+};
+
+const getStartTime = (shortcode: string) => {
+ const shortcode_info = extractInfoFromShortcode(shortcode);
+ if (shortcode_info?.multiplier) return false;
+ return shortcode_info?.start_time || '';
+};
+
+export const isForwardStarting = (shortcode: string, purchase_time?: number) => {
+ const start_time = getStartTime(shortcode);
+ return start_time && purchase_time && /f$/gi.test(start_time);
+};
+
+export const hasForwardContractStarted = (shortcode: string) => {
+ const start_time = getStartTime(shortcode);
+ return start_time && Date.now() / 1000 > Number(start_time.slice(0, -1));
+};
diff --git a/src/components/shared/utils/storage/index.ts b/src/components/shared/utils/storage/index.ts
new file mode 100644
index 00000000..85674ee7
--- /dev/null
+++ b/src/components/shared/utils/storage/index.ts
@@ -0,0 +1 @@
+export * from './storage';
diff --git a/src/components/shared/utils/storage/storage.ts b/src/components/shared/utils/storage/storage.ts
new file mode 100644
index 00000000..ea9f8ea4
--- /dev/null
+++ b/src/components/shared/utils/storage/storage.ts
@@ -0,0 +1,234 @@
+import Cookies from 'js-cookie';
+
+import { getPropertyValue, isEmptyObject } from '../object/object';
+import { deriv_urls } from '../url/constants';
+
+type TCookieStorageThis = {
+ initialized: boolean;
+ cookie_name: string;
+ domain: string;
+ path: string;
+ expires: Date;
+ value: unknown;
+};
+
+const getObject = function (this: { getItem: (key: string) => string | null }, key: string) {
+ return JSON.parse(this.getItem(key) || '{}');
+};
+
+const setObject = function (this: { setItem: (key: string, value: string) => void }, key: string, value: unknown) {
+ if (value && value instanceof Object) {
+ try {
+ this.setItem(key, JSON.stringify(value));
+ } catch (e) {
+ /* do nothing */
+ }
+ }
+};
+
+if (typeof Storage !== 'undefined') {
+ Storage.prototype.getObject = getObject;
+ Storage.prototype.setObject = setObject;
+}
+
+export const isStorageSupported = (storage: Storage) => {
+ if (typeof storage === 'undefined') {
+ return false;
+ }
+
+ const test_key = 'test';
+ try {
+ storage.setItem(test_key, '1');
+ storage.removeItem(test_key);
+ return true;
+ } catch (e) {
+ return false;
+ }
+};
+
+const Store = function (this: { storage: Storage }, storage: Storage) {
+ this.storage = storage;
+ this.storage.getObject = getObject;
+ this.storage.setObject = setObject;
+};
+
+Store.prototype = {
+ get(key: string) {
+ return this.storage.getItem(key) || undefined;
+ },
+ set(key: string, value: string) {
+ if (typeof value !== 'undefined') {
+ this.storage.setItem(key, value);
+ }
+ },
+ getObject(key: string) {
+ return typeof this.storage.getObject === 'function' // Prevent runtime error in IE
+ ? this.storage.getObject(key)
+ : JSON.parse(this.storage.getItem(key) || '{}');
+ },
+ setObject(key: string, value: unknown) {
+ if (typeof this.storage.setObject === 'function') {
+ // Prevent runtime error in IE
+ this.storage.setObject(key, value);
+ } else {
+ this.storage.setItem(key, JSON.stringify(value));
+ }
+ },
+ remove(key: string) {
+ this.storage.removeItem(key);
+ },
+ clear() {
+ this.storage.clear();
+ },
+};
+
+const InScriptStore = function (this: { store: unknown }, object?: unknown) {
+ this.store = typeof object !== 'undefined' ? object : {};
+};
+
+InScriptStore.prototype = {
+ get(key: string) {
+ return getPropertyValue(this.store, key);
+ },
+ set(
+ this: { store: any; set: (key: string | string[], value: string, obj: string[]) => void },
+ k: string | string[],
+ value: string,
+ obj = this.store
+ ) {
+ let key = k;
+ if (!Array.isArray(key)) key = [key];
+ if (key.length > 1) {
+ if (!(key[0] in obj) || isEmptyObject(obj[key[0]])) obj[key[0]] = {};
+ this.set(key.slice(1), value, obj[key[0]]);
+ } else {
+ obj[key[0]] = value;
+ }
+ },
+ getObject(key: string) {
+ return JSON.parse(this.get(key) || '{}');
+ },
+ setObject(key: string, value: unknown) {
+ this.set(key, JSON.stringify(value));
+ },
+ remove(...keys: string[]) {
+ keys.forEach(key => {
+ delete this.store[key];
+ });
+ },
+ clear() {
+ this.store = {};
+ },
+ has(key: string) {
+ return this.get(key) !== undefined;
+ },
+ keys() {
+ return Object.keys(this.store);
+ },
+ call(key: string) {
+ if (typeof this.get(key) === 'function') this.get(key)();
+ },
+};
+
+export const State = new (InScriptStore as any)();
+State.prototype = InScriptStore.prototype;
+/**
+ * Shorthand function to get values from response object of State
+ *
+ * @param {String} pathname
+ * e.g. getResponse('authorize.currency') == get(['response', 'authorize', 'authorize', 'currency'])
+ */
+State.prototype.getResponse = function (pathname: string | string[]) {
+ let path = pathname;
+ if (typeof path === 'string') {
+ const keys = path.split('.');
+ path = ['response', keys[0]].concat(keys);
+ }
+ return this.get(path);
+};
+State.prototype.getByMsgType = State.getResponse;
+State.set('response', {});
+
+export const CookieStorage = function (this: TCookieStorageThis, cookie_name: string, cookie_domain?: string) {
+ const hostname = window.location.hostname;
+
+ this.initialized = false;
+ this.cookie_name = cookie_name;
+ this.domain =
+ cookie_domain ||
+ /* eslint-disable no-nested-ternary */
+ (hostname.includes('binary.sx') ? 'binary.sx' : deriv_urls.DERIV_HOST_NAME);
+ /* eslint-enable no-nested-ternary */
+ this.path = '/';
+ this.expires = new Date('Thu, 1 Jan 2037 12:00:00 GMT');
+ this.value = {};
+};
+
+CookieStorage.prototype = {
+ read() {
+ const cookie_value = Cookies.get(this.cookie_name);
+ try {
+ this.value = cookie_value ? JSON.parse(cookie_value) : {};
+ } catch (e) {
+ this.value = {};
+ }
+ this.initialized = true;
+ },
+ write(val: string, expireDate: Date, isSecure: boolean) {
+ if (!this.initialized) this.read();
+ this.value = val;
+ if (expireDate) this.expires = expireDate;
+ Cookies.set(this.cookie_name, this.value, {
+ expires: this.expires,
+ path: this.path,
+ domain: this.domain,
+ secure: !!isSecure,
+ });
+ },
+ get(key: string) {
+ if (!this.initialized) this.read();
+ return this.value[key];
+ },
+ set(key: string, val: string) {
+ if (!this.initialized) this.read();
+ this.value[key] = val;
+ Cookies.set(this.cookie_name, this.value, {
+ expires: new Date(this.expires),
+ path: this.path,
+ domain: this.domain,
+ });
+ },
+ remove() {
+ Cookies.remove(this.cookie_name, {
+ path: this.path,
+ domain: this.domain,
+ });
+ },
+};
+
+export const removeCookies = (...cookie_names: string[]) => {
+ const domains = [`.${document.domain.split('.').slice(-2).join('.')}`, `.${document.domain}`];
+
+ let parent_path = window.location.pathname.split('/', 2)[1];
+ if (parent_path !== '') {
+ parent_path = `/${parent_path}`;
+ }
+
+ cookie_names.forEach(c => {
+ Cookies.remove(c, { path: '/', domain: domains[0] });
+ Cookies.remove(c, { path: '/', domain: domains[1] });
+ Cookies.remove(c);
+ if (new RegExp(c).test(document.cookie) && parent_path) {
+ Cookies.remove(c, { path: parent_path, domain: domains[0] });
+ Cookies.remove(c, { path: parent_path, domain: domains[1] });
+ Cookies.remove(c, { path: parent_path });
+ }
+ });
+};
+
+export const LocalStore = isStorageSupported(window.localStorage)
+ ? new (Store as any)(window.localStorage)
+ : new (InScriptStore as any)();
+export const SessionStore = isStorageSupported(window.sessionStorage)
+ ? new (Store as any)(window.sessionStorage)
+ : new (InScriptStore as any)();
diff --git a/src/components/shared/utils/string/index.ts b/src/components/shared/utils/string/index.ts
new file mode 100644
index 00000000..472f2c39
--- /dev/null
+++ b/src/components/shared/utils/string/index.ts
@@ -0,0 +1 @@
+export * from './string_util';
diff --git a/src/components/shared/utils/string/string_util.ts b/src/components/shared/utils/string/string_util.ts
new file mode 100644
index 00000000..b7edeb6b
--- /dev/null
+++ b/src/components/shared/utils/string/string_util.ts
@@ -0,0 +1,68 @@
+export const toTitleCase = (str: string) =>
+ (str || '').replace(/\w[^\s/\\]*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
+
+export const padLeft = (txt: string, len: number, char: string) => {
+ const text = String(txt || '');
+ return text.length >= len ? text : `${Array(len - text.length + 1).join(char)}${text}`;
+};
+
+export const compareBigUnsignedInt = (a: number, b?: number | string | null) => {
+ let first_num = numberToString(a);
+ let second_num = numberToString(b);
+ if (!first_num || !second_num) {
+ return '';
+ }
+ const max_length = Math.max(first_num.length, second_num.length);
+ first_num = padLeft(first_num, max_length, '0');
+ second_num = padLeft(second_num, max_length, '0');
+
+ // lexicographical comparison
+ let order = 0;
+ if (first_num !== second_num) {
+ order = first_num > second_num ? 1 : -1;
+ }
+
+ return order;
+};
+
+export const matchStringByChar = (s: string, p: string) => {
+ if (p?.length < 1) return true;
+ const z = p.split('').reduce((a, b) => `${a}[^${b}]*${b}`, '');
+ return RegExp(z, 'i').test(s);
+};
+
+export const numberToString = (n?: number | string | null) => (typeof n === 'number' ? String(n) : n);
+
+export const getKebabCase = (str?: string) => {
+ if (!str) return str;
+ return str
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // get all lowercase letters that are near to uppercase ones
+ .replace(/[\s]+/g, '-') // replace all spaces and low dash
+ .toLowerCase();
+};
+
+// Automatically formats input string with separators based on example format argument.
+export const formatInput = (example_format: string, input_string: string, separator: string) => {
+ const separator_index = example_format.indexOf(separator);
+ const format_count = getCharCount(example_format, separator);
+ const input_count = getCharCount(input_string, separator);
+
+ if (separator_index !== -1 && input_count < format_count && input_string.length - 1 >= separator_index) {
+ return input_string.slice(0, separator_index) + separator + input_string.slice(separator_index);
+ }
+
+ return input_string;
+};
+
+export const getCharCount = (target_string: string, char: string | RegExp) =>
+ target_string.match(new RegExp(char, 'g'))?.length || 0;
+
+export const capitalizeFirstLetter = (target_string: string) =>
+ target_string && target_string[0].toUpperCase() + target_string.slice(1);
+
+export const getEnglishCharacters = (input: string) =>
+ input
+ .normalize('NFD')
+ .split('')
+ .filter(char => /^[a-z ]*$/i.test(char))
+ .join('');
diff --git a/src/components/shared/utils/types.ts b/src/components/shared/utils/types.ts
new file mode 100644
index 00000000..6cd656b3
--- /dev/null
+++ b/src/components/shared/utils/types.ts
@@ -0,0 +1,5 @@
+import { EMPLOYMENT_VALUES, Jurisdiction } from './constants';
+
+export type TBrokerCodes = (typeof Jurisdiction)[keyof typeof Jurisdiction];
+
+export type TEmploymentStatus = (typeof EMPLOYMENT_VALUES)[keyof typeof EMPLOYMENT_VALUES];
diff --git a/src/components/shared/utils/url/constants.ts b/src/components/shared/utils/url/constants.ts
new file mode 100644
index 00000000..20c966c5
--- /dev/null
+++ b/src/components/shared/utils/url/constants.ts
@@ -0,0 +1,26 @@
+const isBrowser = () => typeof window !== 'undefined';
+
+const deriv_com_url = 'deriv.com';
+const deriv_me_url = 'deriv.me';
+const deriv_be_url = 'deriv.be';
+
+const supported_domains = [deriv_com_url, deriv_me_url, deriv_be_url];
+const domain_url_initial = (isBrowser() && window.location.hostname.split('app.')[1]) || '';
+const domain_url = supported_domains.includes(domain_url_initial) ? domain_url_initial : deriv_com_url;
+
+export const deriv_urls = Object.freeze({
+ DERIV_HOST_NAME: domain_url,
+ DERIV_COM_PRODUCTION: `https://${domain_url}`,
+ DERIV_COM_PRODUCTION_EU: `https://eu.${domain_url}`,
+ DERIV_COM_STAGING: `https://staging.${domain_url}`,
+ DERIV_APP_PRODUCTION: `https://app.${domain_url}`,
+ DERIV_APP_STAGING: `https://staging-app.${domain_url}`,
+ SMARTTRADER_PRODUCTION: `https://smarttrader.${domain_url}`,
+ SMARTTRADER_STAGING: `https://staging-smarttrader.${domain_url}`,
+ BINARYBOT_PRODUCTION: `https://bot.${domain_url}`,
+ BINARYBOT_STAGING: `https://staging-bot.${domain_url}`,
+});
+/**
+ * @deprecated Please use 'URLConstants.whatsApp' from '@deriv-com/utils' instead of this.
+ */
+export const whatsapp_url = 'https://wa.me/35699578341';
diff --git a/src/components/shared/utils/url/helpers.ts b/src/components/shared/utils/url/helpers.ts
new file mode 100644
index 00000000..b2815ad9
--- /dev/null
+++ b/src/components/shared/utils/url/helpers.ts
@@ -0,0 +1,75 @@
+import { deriv_urls } from './constants';
+
+/**
+ * @deprecated Please use 'URLUtils.getQueryParameter' from '@deriv-com/utils' instead of this.
+ */
+export const getlangFromUrl = () => {
+ const queryString = window.location.search;
+ const urlParams = new URLSearchParams(queryString);
+ const lang = urlParams.get('lang');
+ return lang;
+};
+
+/**
+ * @deprecated Please use 'URLUtils.getQueryParameter' from '@deriv-com/utils' instead of this.
+ */
+export const getActionFromUrl = () => {
+ const queryString = window.location.search;
+ const urlParams = new URLSearchParams(queryString);
+ const action = urlParams.get('action');
+ return action;
+};
+
+export const getUrlSmartTrader = () => {
+ const { is_staging_deriv_app } = getPlatformFromUrl();
+ const url_lang = getlangFromUrl();
+ const i18n_language = window.localStorage.getItem('i18n_language') || url_lang || 'en';
+
+ let base_link = '';
+
+ if (is_staging_deriv_app) {
+ base_link = deriv_urls.SMARTTRADER_STAGING;
+ } else {
+ base_link = deriv_urls.SMARTTRADER_PRODUCTION;
+ }
+
+ return `${base_link}/${i18n_language.toLowerCase()}/trading.html`;
+};
+
+export const getUrlBinaryBot = (is_language_required = true) => {
+ const { is_staging_deriv_app } = getPlatformFromUrl();
+
+ const url_lang = getlangFromUrl();
+ const i18n_language = window.localStorage.getItem('i18n_language') || url_lang || 'en';
+
+ const base_link = is_staging_deriv_app ? deriv_urls.BINARYBOT_STAGING : deriv_urls.BINARYBOT_PRODUCTION;
+
+ return is_language_required ? `${base_link}/?l=${i18n_language.toLowerCase()}` : base_link;
+};
+
+export const getPlatformFromUrl = (domain = window.location.hostname) => {
+ const resolutions = {
+ is_staging_deriv_app: /^staging-app\.deriv\.(com|me|be)$/i.test(domain),
+ is_deriv_app: /^app\.deriv\.(com|me|be)$/i.test(domain),
+ is_test_link: /^(.*)\.binary\.sx$/i.test(domain),
+ is_test_deriv_app: /^test-app\.deriv\.com$/i.test(domain),
+ };
+
+ return {
+ ...resolutions,
+ is_staging: resolutions.is_staging_deriv_app,
+ is_test_link: resolutions.is_test_link,
+ };
+};
+
+export const isStaging = (domain = window.location.hostname) => {
+ const { is_staging_deriv_app } = getPlatformFromUrl(domain);
+
+ return is_staging_deriv_app;
+};
+
+export const isTestDerivApp = (domain = window.location.hostname) => {
+ const { is_test_deriv_app } = getPlatformFromUrl(domain);
+
+ return is_test_deriv_app;
+};
diff --git a/src/components/shared/utils/url/index.ts b/src/components/shared/utils/url/index.ts
new file mode 100644
index 00000000..c9506f57
--- /dev/null
+++ b/src/components/shared/utils/url/index.ts
@@ -0,0 +1,3 @@
+export * from './constants';
+export * from './url';
+export * from './helpers';
diff --git a/src/components/shared/utils/url/url.ts b/src/components/shared/utils/url/url.ts
new file mode 100644
index 00000000..1598e00c
--- /dev/null
+++ b/src/components/shared/utils/url/url.ts
@@ -0,0 +1,186 @@
+import { getCurrentProductionDomain } from '../config/config';
+import { routes } from '../routes';
+
+import { deriv_urls } from './constants';
+import { getPlatformFromUrl } from './helpers';
+
+type TOption = {
+ query_string?: string;
+ legacy?: boolean;
+ language?: string;
+};
+
+const default_domain = 'binary.com';
+const host_map = {
+ // the exceptions regarding updating the URLs
+ 'bot.binary.com': 'www.binary.bot',
+ 'developers.binary.com': 'developers.binary.com', // same, shouldn't change
+ 'academy.binary.com': 'academy.binary.com',
+ 'blog.binary.com': 'blog.binary.com',
+};
+
+let location_url: Location, default_language: string;
+
+export const legacyUrlForLanguage = (target_language: string, url: string = window.location.href) =>
+ url.replace(new RegExp(`/${default_language}/`, 'i'), `/${(target_language || 'EN').trim().toLowerCase()}/`);
+
+export const urlForLanguage = (lang: string, url: string = window.location.href) => {
+ const current_url = new URL(url);
+
+ if (lang === 'EN') {
+ current_url.searchParams.delete('lang');
+ } else {
+ current_url.searchParams.set('lang', lang);
+ }
+
+ return `${current_url}`;
+};
+
+export const reset = () => {
+ location_url = window?.location ?? location_url;
+};
+
+export const params = (href?: string | URL) => {
+ const arr_params = [];
+ const parsed = ((href ? new URL(href) : location_url).search || '').substr(1).split('&');
+ let p_l = parsed.length;
+ while (p_l--) {
+ const param = parsed[p_l].split('=');
+ arr_params.push(param);
+ }
+ return arr_params;
+};
+
+/**
+ * @deprecated Please use 'URLUtils.normalizePath' from '@deriv-com/utils' instead of this.
+ */
+export const normalizePath = (path: string) => (path ? path.replace(/(^\/|\/$|[^a-zA-Z0-9-_./()#])/g, '') : '');
+
+export const urlFor = (
+ path: string,
+ options: TOption = {
+ query_string: undefined,
+ legacy: false,
+ language: undefined,
+ }
+) => {
+ const { legacy, language, query_string } = options;
+
+ if (legacy && /^bot$/.test(path)) {
+ return `https://${host_map['bot.binary.com']}`;
+ }
+
+ const lang = language?.toLowerCase?.() ?? default_language;
+ let domain = `https://${window.location.hostname}/`;
+ if (legacy) {
+ if (getPlatformFromUrl().is_staging_deriv_app) {
+ domain = domain.replace(/staging-app\.deriv\.com/, `staging.binary.com/${lang || 'en'}`);
+ } else if (getPlatformFromUrl().is_deriv_app) {
+ domain = domain.replace(/app\.deriv\.com/, `binary.com/${lang || 'en'}`);
+ } else {
+ domain = `https://binary.com/${lang || 'en'}/`;
+ }
+ }
+ const new_url = `${domain}${normalizePath(path) || 'home'}.html${query_string ? `?${query_string}` : ''}`;
+
+ if (lang && !legacy) {
+ return urlForLanguage(lang, new_url);
+ } else if (legacy) {
+ return legacyUrlForLanguage(lang, new_url);
+ }
+
+ return new_url;
+};
+
+export const urlForCurrentDomain = (href: string) => {
+ const current_domain = getCurrentProductionDomain();
+
+ if (!current_domain) {
+ return href; // don't change when domain is not supported
+ }
+
+ const url_object = new URL(href);
+ if (Object.keys(host_map).includes(url_object.hostname)) {
+ url_object.hostname = host_map[url_object.hostname as keyof typeof host_map];
+ } else if (url_object.hostname.match(default_domain)) {
+ // to keep all non-Binary links unchanged, we use default domain for all Binary links in the codebase (javascript and templates)
+ url_object.hostname = url_object.hostname.replace(
+ new RegExp(`\\.${default_domain}`, 'i'),
+ `.${current_domain}`
+ );
+ } else {
+ return href;
+ }
+
+ return url_object.href;
+};
+
+export const websiteUrl = () => `${location.protocol}//${location.hostname}/`;
+
+export const getUrlBase = (path = '') => {
+ const l = window.location;
+
+ if (!/^\/(br_)/.test(l.pathname)) return path;
+
+ return `/${l.pathname.split('/')[1]}${/^\//.test(path) ? path : `/${path}`}`;
+};
+
+export const removeBranchName = (path = '') => {
+ return path.replace(/^\/br_.*?\//, '/');
+};
+
+export const getHostMap = () => host_map;
+
+export const setUrlLanguage = (lang: string) => {
+ default_language = lang;
+};
+
+// TODO: cleanup options param usage
+// eslint-disable-next-line no-unused-vars
+/**
+ * @deprecated Please use 'URLUtils.getDerivStaticURL' from '@deriv-com/utils' instead of this.
+ */
+export const getStaticUrl = (path = '', is_document = false, is_eu_url = false) => {
+ const host = is_eu_url ? deriv_urls.DERIV_COM_PRODUCTION_EU : deriv_urls.DERIV_COM_PRODUCTION;
+ let lang = default_language?.toLowerCase();
+
+ if (lang && lang !== 'en') {
+ lang = `/${lang}`;
+ } else {
+ lang = '';
+ }
+
+ if (is_document) return `${host}/${normalizePath(path)}`;
+
+ // Deriv.com supports languages separated by '-' not '_'
+ if (host === deriv_urls.DERIV_COM_PRODUCTION && lang.includes('_')) {
+ lang = lang.replace('_', '-');
+ }
+
+ return `${host}${lang}/${normalizePath(path)}`;
+};
+
+export const getPath = (route_path: string, parameters = {}) =>
+ Object.keys(parameters).reduce(
+ (p, name) => p.replace(`:${name}`, parameters[name as keyof typeof parameters]),
+ route_path
+ );
+
+export const getContractPath = (contract_id?: number) => getPath(routes.contract, { contract_id });
+
+/**
+ * Filters query string. Returns filtered query (without '/?')
+ * @param {string} search_param window.location.search
+ * @param {Array} allowed_keys array of string of allowed query string keys
+ */
+export const filterUrlQuery = (search_param: string, allowed_keys: string[]) => {
+ const search_params = new URLSearchParams(search_param);
+ const filtered_queries = [...search_params].filter(kvp => allowed_keys.includes(kvp[0]));
+ return new URLSearchParams(filtered_queries || '').toString();
+};
+
+export const excludeParamsFromUrlQuery = (search_param: string, excluded_keys: string[]) => {
+ const search_params = new URLSearchParams(search_param);
+ const filtered_queries = [...search_params].filter(([key]) => !excluded_keys.includes(key));
+ return filtered_queries.length ? `?${new URLSearchParams(filtered_queries).toString()}` : '';
+};
diff --git a/src/components/shared/utils/validation/declarative-validation-rules.ts b/src/components/shared/utils/validation/declarative-validation-rules.ts
new file mode 100644
index 00000000..a79d7a96
--- /dev/null
+++ b/src/components/shared/utils/validation/declarative-validation-rules.ts
@@ -0,0 +1,195 @@
+import { addComma } from '../currency';
+import { cloneObject } from '../object';
+import { compareBigUnsignedInt } from '../string';
+
+import { TFormErrorMessagesTypes } from './form-error-messages-types';
+
+export type TOptions = {
+ [key: string]: unknown;
+ decimals?: string | number;
+ is_required?: boolean;
+ max?: number | string | null;
+ min?: number | string | null;
+ name1?: string;
+ name2?: string;
+ regex?: RegExp;
+ type?: string;
+};
+
+export type TRuleOptions = {
+ func?: (
+ value: T,
+ options?: TOptions,
+ store?: S,
+ inputs?: Pick
+ ) => boolean | { is_ok: boolean; message: string };
+ condition?: (store: S) => boolean;
+ message?: string;
+} & TOptions;
+
+const validRequired = (value?: string | number /* , options, field */) => {
+ if (value === undefined || value === null) {
+ return false;
+ }
+
+ const str = value.toString().replace(/\s/g, '');
+ return str.length > 0;
+};
+export const address_permitted_special_characters_message = ". , ' : ; ( ) ° @ # / -";
+export const validAddress = (value: string, options?: TOptions) => {
+ if (options?.is_required && (!value || value.match(/^\s*$/))) {
+ return {
+ is_ok: false,
+ message: form_error_messages?.empty_address(),
+ };
+ } else if (!validLength(value, { min: 0, max: 70 })) {
+ return {
+ is_ok: false,
+ message: form_error_messages?.maxNumber(70),
+ };
+ } else if (!/^[\p{L}\p{Nd}\s'.,:;()\u00b0@#/-]{0,70}$/u.test(value)) {
+ return {
+ is_ok: false,
+ message: form_error_messages?.address(),
+ };
+ }
+ return { is_ok: true };
+};
+export const validPostCode = (value: string) => value === '' || /^[A-Za-z0-9][A-Za-z0-9\s-]*$/.test(value);
+export const validTaxID = (value: string) => /(?!^$|\s+)[A-Za-z0-9./\s-]$/.test(value);
+export const validPhone = (value: string) => /^\+?([0-9-]+\s)*[0-9-]+$/.test(value);
+export const validLetterSymbol = (value: string) => /^[A-Za-z]+([a-zA-Z.' -])*[a-zA-Z.' -]+$/.test(value);
+export const validName = (value: string) => /^(?!.*\s{2,})[\p{L}\s'.-]{2,50}$/u.test(value);
+// eslint-disable-next-line default-param-last
+export const validLength = (value = '', options: TOptions) =>
+ (options.min ? value.length >= Number(options.min) : true) &&
+ (options.max ? value.length <= Number(options.max) : true);
+export const validPassword = (value: string) => /^(?=.*[a-z])(?=.*\d)(?=.*[A-Z])[!-~]{8,25}$/.test(value);
+export const validEmail = (value: string) => /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/.test(value);
+const validBarrier = (value: string) => /^[+-]?\d+\.?\d*$/.test(value);
+const validGeneral = (value: string) => !/[`~!@#$%^&*)(_=+[}{\]\\/";:?><|]+/.test(value);
+const validRegular = (value: string, options: TOptions) => options.regex?.test(value);
+const confirmRequired = (value: string) => !!value;
+const checkPOBox = (value: string) => !/p[.\s]+o[.\s]+box/i.test(value);
+const validEmailToken = (value: string) => value.trim().length === 8;
+export const hasInvalidCharacters = (target_string: string) => /[^\dX\s]/.test(target_string);
+export const isFormattedCardNumber = (target_string: string) =>
+ /(^\d{4})\s(\d{2}X{2})\s(X{4})\s(\d{4}$)/.test(target_string);
+export const validFile = (file: File) => file?.type && /(image|application)\/(jpe?g|pdf|png)$/.test(file?.type);
+export const validMT5Password = (value: string) => /^(?=.*[!@#$%^&*()+\-=[\]{};':"|,.<>/?_~])[ -~]{8,16}$/.test(value);
+
+let pre_build_dvrs: TInitPreBuildDVRs, form_error_messages: TFormErrorMessagesTypes;
+
+const isMoreThanMax = (value: number, options: TOptions) =>
+ options.type === 'float' ? +value > Number(options.max) : compareBigUnsignedInt(value, options.max) === 1;
+
+export const validNumber = (value: string, opts: TOptions) => {
+ const options = cloneObject(opts);
+ let message = null;
+ if (options.allow_empty && value.length === 0) {
+ return {
+ is_ok: true,
+ };
+ }
+
+ let is_ok = true;
+ if ('min' in options && typeof options.min === 'function') {
+ options.min = options.min();
+ }
+ if ('max' in options && typeof options.max === 'function') {
+ options.max = options.max();
+ }
+
+ if (!(options.type === 'float' ? /^\d*(\.\d+)?$/ : /^\d+$/).test(value) || isNaN(+value)) {
+ is_ok = false;
+ message = form_error_messages.number();
+ } else if ('min' in options && 'max' in options && +options.min === +options.max && +value !== +options.min) {
+ is_ok = false;
+ message = form_error_messages.value(addComma(options.min, options.decimals));
+ } else if (
+ 'min' in options &&
+ 'max' in options &&
+ options.min > 0 &&
+ (+value < +options.min || isMoreThanMax(+value, options))
+ ) {
+ is_ok = false;
+ const min_value = addComma(options.min, options.decimals);
+ const max_value = addComma(options.max, options.decimals);
+ message = form_error_messages.betweenMinMax(min_value, max_value);
+ } else if (
+ options.type === 'float' &&
+ options.decimals &&
+ !new RegExp(`^\\d+(\\.\\d{0,${options.decimals}})?$`).test(value)
+ ) {
+ is_ok = false;
+ message = form_error_messages.decimalPlaces(options.decimals);
+ } else if ('min' in options && +value < +options.min) {
+ is_ok = false;
+ const min_value = addComma(options.min, options.decimals);
+ message = form_error_messages.minNumber(min_value);
+ } else if ('max' in options && isMoreThanMax(+value, options)) {
+ is_ok = false;
+ const max_value = addComma(options.max, options.decimals);
+ message = form_error_messages.maxNumber(max_value);
+ }
+ return { is_ok, message };
+};
+
+export type TInitPreBuildDVRs = ReturnType;
+const initPreBuildDVRs = () => ({
+ address: {
+ func: validAddress,
+ message: form_error_messages.address,
+ },
+ barrier: {
+ func: validBarrier,
+ message: form_error_messages.barrier,
+ },
+ email: { func: validEmail, message: form_error_messages.email },
+ general: {
+ func: validGeneral,
+ message: form_error_messages.general,
+ },
+ length: { func: validLength, message: '' }, // Message will be set in validLength function on initiation
+ name: {
+ func: validName,
+ message: form_error_messages.name,
+ },
+ number: {
+ func: (...args: [string, TOptions, Record]) => {
+ const [value, opts] = args;
+ return validNumber(value, opts);
+ },
+ message: form_error_messages.number,
+ },
+ password: {
+ func: validPassword,
+ message: form_error_messages.password,
+ },
+ phone: { func: validPhone, message: form_error_messages.phone },
+ po_box: { func: checkPOBox, message: form_error_messages.po_box },
+ postcode: { func: validPostCode, message: form_error_messages.postcode },
+ regular: { func: validRegular, message: '' },
+ req: { func: validRequired, message: '' },
+ confirm: { func: confirmRequired, message: '' },
+ signup_token: { func: validEmailToken, message: form_error_messages.signup_token },
+ tax_id: {
+ func: validTaxID,
+ message: form_error_messages.validTaxID,
+ },
+});
+
+export const initFormErrorMessages = (all_form_error_messages: TFormErrorMessagesTypes) => {
+ if (!pre_build_dvrs) {
+ form_error_messages = all_form_error_messages;
+ pre_build_dvrs = initPreBuildDVRs();
+ }
+};
+
+export const getPreBuildDVRs = () => {
+ return pre_build_dvrs;
+};
+
+export const getErrorMessages = () => {
+ return form_error_messages;
+};
diff --git a/src/components/shared/utils/validation/form-error-messages-types.ts b/src/components/shared/utils/validation/form-error-messages-types.ts
new file mode 100644
index 00000000..b04937db
--- /dev/null
+++ b/src/components/shared/utils/validation/form-error-messages-types.ts
@@ -0,0 +1,57 @@
+type TMessage = () => string;
+type TParameter = string | number;
+
+export type TFormErrorMessagesTypes = Record<
+ | 'empty_address'
+ | 'address'
+ | 'barrier'
+ | 'email'
+ | 'general'
+ | 'name'
+ | 'password'
+ | 'po_box'
+ | 'phone'
+ | 'postcode'
+ | 'signup_token'
+ | 'tax_id'
+ | 'number'
+ | 'validTaxID',
+ TMessage
+> & {
+ decimalPlaces: (decimals: TParameter) => string;
+ value: (value: TParameter) => string;
+ betweenMinMax: (min_value: TParameter, max_value: TParameter) => string;
+ minNumber: (min_value: TParameter) => string;
+ maxNumber: (max_value: TParameter) => string;
+ password_warnings: Record<
+ | 'use_a_few_words'
+ | 'no_need_for_mixed_chars'
+ | 'uncommon_words_are_better'
+ | 'straight_rows_of_keys_are_easy'
+ | 'short_keyboard_patterns_are_easy'
+ | 'use_longer_keyboard_patterns'
+ | 'repeated_chars_are_easy'
+ | 'repeated_patterns_are_easy'
+ | 'avoid_repeated_chars'
+ | 'sequences_are_easy'
+ | 'avoid_sequences'
+ | 'recent_years_are_easy'
+ | 'avoid_recent_years'
+ | 'avoid_associated_years'
+ | 'dates_are_easy'
+ | 'avoid_associated_dates_and_years'
+ | 'top10_common_password'
+ | 'top100_common_password'
+ | 'very_common_password'
+ | 'similar_to_common_password'
+ | 'a_word_is_easy'
+ | 'names_are_easy'
+ | 'common_names_are_easy'
+ | 'capitalization_doesnt_help'
+ | 'all_uppercase_doesnt_help'
+ | 'reverse_doesnt_help'
+ | 'substitution_doesnt_help'
+ | 'user_dictionary',
+ TMessage
+ >;
+};
diff --git a/src/components/shared/utils/validation/form-validations.ts b/src/components/shared/utils/validation/form-validations.ts
new file mode 100644
index 00000000..1034cfdf
--- /dev/null
+++ b/src/components/shared/utils/validation/form-validations.ts
@@ -0,0 +1,116 @@
+import fromEntries from 'object.fromentries';
+
+import { EMPLOYMENT_VALUES } from '../constants';
+import { TEmploymentStatus } from '../types';
+
+import { getPreBuildDVRs, TInitPreBuildDVRs, TOptions } from './declarative-validation-rules';
+
+type TConfig = {
+ default_value: string | boolean | number;
+ supported_in: string[];
+ rules?: Array<(TOptions | any)[]>;
+ values?: Record;
+};
+export type TSchema = { [key: string]: TConfig };
+
+/**
+ * Prepare default field and names for form.
+ * @param {string} landing_company
+ * @param {object} schema
+ */
+export const getDefaultFields = (landing_company: string, schema: TSchema | Record) => {
+ const output: { [key: string]: string | number | boolean } = {};
+ Object.entries(filterByLandingCompany(landing_company, schema)).forEach(([field_name, opts]) => {
+ output[field_name] = opts.default_value;
+ });
+ return output;
+};
+
+export const filterByLandingCompany = (landing_company: string, schema: TSchema | Record) =>
+ fromEntries(Object.entries(schema).filter(([, opts]) => opts.supported_in.includes(landing_company)));
+
+/**
+ * Generate validation function for the landing_company
+ * @param landing_company
+ * @param schema
+ * @return {function(*=): {}}
+ */
+export const generateValidationFunction = (landing_company: string, schema: TSchema) => {
+ const rules_schema = landing_company ? filterByLandingCompany(landing_company, schema) : schema;
+ const rules: { [key: string]: TConfig['rules'] } = {};
+ Object.entries(rules_schema).forEach(([key, opts]) => {
+ rules[key] = opts.rules;
+ });
+
+ return (values: { [key: string]: string }) => {
+ const errors: { [key: string]: string | string[] } = {};
+
+ Object.entries(values).forEach(([field_name, value]) => {
+ if (field_name in rules) {
+ rules[field_name]?.some(([rule, message, options]) => {
+ if (
+ checkForErrors({
+ field_name,
+ value,
+ rule,
+ options,
+ values,
+ })
+ ) {
+ errors[field_name] = typeof message === 'string' ? ['error', message] : message;
+ return true;
+ }
+
+ return false;
+ });
+ }
+ });
+
+ return errors;
+ };
+};
+
+type TCheckForErrors = {
+ field_name: string;
+ value: string;
+ rule: string;
+ options: TOptions;
+ values: Record;
+};
+/**
+ * Returns true if the rule has error, false otherwise.
+ * @param value
+ * @param rule
+ * @param options
+ * @return {boolean}
+ */
+const checkForErrors = ({ value, rule, options, values }: TCheckForErrors) => {
+ const validate = getValidationFunction(rule);
+ return !validate(value, options, values);
+};
+
+/**
+ * Get validation function from rules array
+ * @param rule
+ * @throws Error when validation rule not found
+ * @return {function(*=): *}
+ */
+export const getValidationFunction = (rule: string) => {
+ const func = getPreBuildDVRs()[rule as keyof TInitPreBuildDVRs]?.func ?? rule;
+ if (typeof func !== 'function') {
+ throw new Error(
+ `validation rule ${rule} not found. Available validations are: ${JSON.stringify(
+ Object.keys(getPreBuildDVRs())
+ )}`
+ );
+ }
+ /**
+ * Generated validation function from the DVRs.
+ */
+ return (value: string, options: TOptions, values: Record) =>
+ !!func(value, options, values);
+};
+
+// Adding string as type because, employment_status can come from Personal details or Financial assessment.
+export const shouldHideOccupationField = (employment_status?: TEmploymentStatus | string) =>
+ EMPLOYMENT_VALUES.UNEMPLOYED === employment_status;
diff --git a/src/components/shared/utils/validation/index.ts b/src/components/shared/utils/validation/index.ts
new file mode 100644
index 00000000..d83e0862
--- /dev/null
+++ b/src/components/shared/utils/validation/index.ts
@@ -0,0 +1,3 @@
+export * from './declarative-validation-rules';
+export * from './form-validations';
+export * from './regex-validation';
diff --git a/src/components/shared/utils/validation/regex-validation.ts b/src/components/shared/utils/validation/regex-validation.ts
new file mode 100644
index 00000000..0a586922
--- /dev/null
+++ b/src/components/shared/utils/validation/regex-validation.ts
@@ -0,0 +1,9 @@
+export const regex_checks = {
+ address_details: {
+ address_city: /^\p{L}[\p{L}\s'.-]{0,99}$/u,
+ address_line_1: /^[\p{L}\p{Nd}\s'.,:;()\u00b0@#/-]{1,70}$/u,
+ address_line_2: /^[\p{L}\p{Nd}\s'.,:;()\u00b0@#/-]{0,70}$/u,
+ address_postcode: /^(?! )[a-zA-Z0-9\s-]{0,20}$/,
+ address_state: /^[\w\s\W'.;,-]{0,99}$/,
+ },
+};
diff --git a/src/components/shared/utils/validator/errors.ts b/src/components/shared/utils/validator/errors.ts
new file mode 100644
index 00000000..23bd74ef
--- /dev/null
+++ b/src/components/shared/utils/validator/errors.ts
@@ -0,0 +1,42 @@
+class Errors {
+ errors: { [key: string]: string[] };
+
+ constructor() {
+ this.errors = {};
+ }
+
+ add(attribute: string, message: string) {
+ if (!this.has(attribute)) {
+ this.errors[attribute] = [];
+ }
+
+ if (this.errors[attribute].indexOf(message) === -1) {
+ this.errors[attribute].push(message);
+ }
+ }
+
+ all() {
+ return this.errors;
+ }
+
+ first(attribute: string) {
+ if (this.has(attribute)) {
+ return this.errors[attribute][0];
+ }
+ return null;
+ }
+
+ get(attribute: string) {
+ if (this.has(attribute)) {
+ return this.errors[attribute];
+ }
+
+ return [];
+ }
+
+ has(attribute: string) {
+ return Object.prototype.hasOwnProperty.call(this.errors, attribute);
+ }
+}
+
+export default Errors;
diff --git a/src/components/shared/utils/validator/index.ts b/src/components/shared/utils/validator/index.ts
new file mode 100644
index 00000000..aa4cdd77
--- /dev/null
+++ b/src/components/shared/utils/validator/index.ts
@@ -0,0 +1,4 @@
+import Errors from './errors';
+import Validator, { template } from './validator';
+
+export { Errors, template, Validator };
diff --git a/src/components/shared/utils/validator/validator.ts b/src/components/shared/utils/validator/validator.ts
new file mode 100644
index 00000000..8f5806ab
--- /dev/null
+++ b/src/components/shared/utils/validator/validator.ts
@@ -0,0 +1,138 @@
+import { getPreBuildDVRs, TRuleOptions } from '../validation/declarative-validation-rules';
+
+import Errors from './errors';
+
+type TRule = string | Array>;
+
+export const template = (string: string, content: string | Array) => {
+ let to_replace = content;
+ if (content && !Array.isArray(content)) {
+ to_replace = [content];
+ }
+ return string.replace(/\[_(\d+)]/g, (s, index) => to_replace[+index - 1]);
+};
+
+class Validator {
+ input: Pick;
+ rules: T;
+ store: S;
+ errors: Errors;
+ error_count: number;
+
+ constructor(input: Pick, rules: T, store: S) {
+ this.input = input;
+ this.rules = rules;
+ this.store = store;
+ this.errors = new Errors();
+
+ this.error_count = 0;
+ }
+
+ /**
+ * Add failure and error message for given rule
+ *
+ * @param {string} attribute
+ * @param {object} rule
+ */
+ addFailure(attribute: string, rule: { name: string; options: TRuleOptions }, error_message?: string) {
+ let message =
+ error_message ||
+ rule.options.message ||
+ (getPreBuildDVRs() as unknown as { [key: string]: { message: () => string } })[rule.name].message();
+ if (rule.name === 'length') {
+ message = template(message, [
+ rule.options.min === rule.options.max
+ ? rule.options.min?.toString() ?? ''
+ : `${rule.options.min}-${rule.options.max}`,
+ ]);
+ } else if (rule.name === 'min') {
+ message = template(message, [rule.options.min?.toString() ?? '']);
+ } else if (rule.name === 'not_equal') {
+ message = template(message, [rule.options.name1 ?? '', rule.options.name2 ?? '']);
+ }
+ this.errors.add(attribute, message);
+ this.error_count++;
+ }
+
+ /**
+ * Runs validator
+ *
+ * @return {boolean} Whether it passes; true = passes, false = fails
+ */
+ check() {
+ Object.keys(this.input).forEach(attribute => {
+ if (!Object.prototype.hasOwnProperty.call(this.rules, attribute)) {
+ return;
+ }
+
+ (this.rules as { [key: string]: Array> })[attribute].forEach((rule: TRule) => {
+ const ruleObject = Validator.getRuleObject(rule);
+
+ if (!ruleObject.validator && typeof ruleObject.validator !== 'function') {
+ return;
+ }
+
+ if (ruleObject.options.condition && !ruleObject.options.condition(this.store)) {
+ return;
+ }
+
+ if (this.input[attribute as keyof S] === '' && ruleObject.name !== 'req') {
+ return;
+ }
+
+ const result = ruleObject.validator(
+ this.input[attribute as keyof S] as string,
+ ruleObject.options,
+ this.store,
+ this.input
+ );
+ if (typeof result === 'boolean' && !result) {
+ this.addFailure(attribute, ruleObject);
+ } else if (typeof result === 'object') {
+ const { is_ok, message } = result;
+ if (!is_ok) {
+ this.addFailure(attribute, ruleObject, message);
+ }
+ }
+ });
+ });
+ return !this.error_count;
+ }
+
+ /**
+ * Determine if validation passes
+ *
+ * @return {boolean}
+ */
+ isPassed() {
+ return this.check();
+ }
+
+ /**
+ * Converts the rule array to an object
+ *
+ * @param {array} rule
+ * @return {object}
+ */
+ static getRuleObject(rule: TRule) {
+ const is_rule_string = typeof rule === 'string';
+ const rule_object_name = (is_rule_string ? rule : rule[0]) as string;
+ const rule_object_options = (is_rule_string ? {} : rule[1] || {}) as TRuleOptions;
+ return {
+ name: rule_object_name,
+ options: rule_object_options,
+ validator:
+ rule_object_name === 'custom'
+ ? rule_object_options.func
+ : (
+ getPreBuildDVRs() as {
+ [key: string]: {
+ func: TRuleOptions['func'];
+ };
+ }
+ )[rule_object_name].func,
+ };
+ }
+}
+
+export default Validator;
diff --git a/src/components/shared_ui/arrow-indicator/arrow-indicator.tsx b/src/components/shared_ui/arrow-indicator/arrow-indicator.tsx
new file mode 100644
index 00000000..dcaaf362
--- /dev/null
+++ b/src/components/shared_ui/arrow-indicator/arrow-indicator.tsx
@@ -0,0 +1,57 @@
+import React from 'react';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+type TArrowIndicatorProps = {
+ className?: string;
+ value?: number | string;
+};
+
+type TData = {
+ previous_icon: string;
+ icon: string;
+ previous_value?: string | number;
+ value?: string | number;
+};
+
+const ArrowIndicator = ({ className, value }: TArrowIndicatorProps) => {
+ const [is_visible, setIsVisible] = React.useState(false);
+ const [data, setData] = React.useState({
+ icon: '',
+ previous_icon: '',
+ });
+ const { icon, previous_icon, previous_value } = data;
+ const has_comparable_values = !isNaN(Number(data.value)) && !isNaN(Number(value));
+ const timeout_id = React.useRef>();
+
+ React.useEffect(() => {
+ setIsVisible(true);
+ setData(prev_data => {
+ const has_increased = Number(prev_data.value) < Number(value);
+ const icon_name = has_increased ? 'IcProfit' : 'IcLoss';
+ return {
+ icon: has_comparable_values ? icon_name : '',
+ previous_icon: prev_data.icon,
+ previous_value: prev_data.value,
+ value,
+ };
+ });
+
+ clearTimeout(timeout_id.current);
+ timeout_id.current = setTimeout(() => {
+ setIsVisible(false);
+ }, 3000);
+ return () => clearTimeout(timeout_id.current);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value]);
+
+ return (
+
+ {has_comparable_values && is_visible ? (
+
+ ) : null}
+
+ );
+};
+
+export default React.memo(ArrowIndicator);
diff --git a/src/components/shared_ui/arrow-indicator/index.ts b/src/components/shared_ui/arrow-indicator/index.ts
new file mode 100644
index 00000000..ff0a2ded
--- /dev/null
+++ b/src/components/shared_ui/arrow-indicator/index.ts
@@ -0,0 +1,3 @@
+import ArrowIndicator from './arrow-indicator';
+
+export default ArrowIndicator;
diff --git a/src/components/shared_ui/autocomplete/autocomplete.scss b/src/components/shared_ui/autocomplete/autocomplete.scss
new file mode 100644
index 00000000..f7b207ba
--- /dev/null
+++ b/src/components/shared_ui/autocomplete/autocomplete.scss
@@ -0,0 +1,60 @@
+// Colors used here are borrowed from input.scss
+// TODO: use-phase-2-colors - Switch to using phase 2 colors, following the new color palette (refer Abstract)
+
+.dc-autocomplete {
+ width: 100%;
+ position: relative;
+
+ &__trailing-icon {
+ position: absolute;
+ right: 0;
+ pointer-events: none;
+ margin-right: 1.1rem;
+ cursor: text;
+ transition: transform 0.2s ease;
+ transform: rotate(0deg);
+ transform-origin: 50% 45%;
+
+ &--opened {
+ transform: rotate(-180deg);
+ }
+ &--disabled {
+ --fill-color1: var(--text-less-prominent) !important;
+ }
+ .color1-fill {
+ fill: var(--text-less-prominent);
+ }
+ }
+ .dc-input {
+ margin-bottom: 0;
+
+ &--error {
+ .dc-autocomplete__trailing-icon {
+ .color1-fill {
+ fill: var(--text-loss-danger);
+ }
+ }
+ }
+ }
+ .dc-input__field {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ .dc-dropdown-list {
+ box-shadow: 0 8px 16px 0 var(--shadow-menu);
+ background: var(--general-main-2);
+ }
+ &:focus,
+ &:focus-within,
+ &:active {
+ outline: 0;
+
+ .dc-input:not(.dc-input--error) {
+ .dc-autocomplete__trailing-icon {
+ .color1-fill {
+ fill: var(--brand-secondary);
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/shared_ui/autocomplete/autocomplete.tsx b/src/components/shared_ui/autocomplete/autocomplete.tsx
new file mode 100644
index 00000000..3b498ed2
--- /dev/null
+++ b/src/components/shared_ui/autocomplete/autocomplete.tsx
@@ -0,0 +1,401 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { getSearchNotFoundOption } from '@/components/shared/utils/constants';
+import { getPosition } from '@/components/shared/utils/dom';
+import { getEnglishCharacters, matchStringByChar } from '@/components/shared/utils/string';
+import { useBlockScroll } from '@/hooks/use-blockscroll';
+import { Icon } from '@/utils/tmp/dummy';
+
+import DropdownList, { TItem } from '../dropdown-list';
+import Input from '../input';
+
+type TAutocompleteProps = {
+ autoComplete: string;
+ className: string;
+ disabled?: boolean;
+ dropdown_offset: string;
+ error?: string;
+ has_updating_list?: boolean;
+ hide_list?: boolean;
+ historyValue: string;
+ input_id: string;
+ is_alignment_top: boolean;
+ is_list_visible?: boolean;
+ list_height: string;
+ list_items: TItem[];
+ list_portal_id: string;
+ not_found_text: string;
+ onBlur?: (e: React.FocusEvent) => void;
+ onHideDropdownList: () => void;
+ onItemSelection: (item: TItem) => void;
+ onScrollStop?: () => void;
+ onShowDropdownList?: () => void;
+ should_filter_by_char: boolean;
+ show_list?: boolean;
+ trailing_icon?: React.ReactElement;
+ value: string;
+ onSearch?: (value: string, items: TItem[]) => [];
+ data_testid: string;
+ readOnly?: boolean;
+};
+
+const KEY_CODE = {
+ ENTER: 13,
+ ESCAPE: 27,
+ TAB: 9,
+ KEYDOWN: 40,
+ KEYUP: 38,
+};
+
+const isString = (item: TItem): item is string => typeof item === 'string';
+
+const getFilteredItems = (val: string, list: TItem[], should_filter_by_char = false) => {
+ const searchTerm = val.toLowerCase();
+
+ const filtered = list.filter(item => {
+ const text = isString(item) ? item.toLowerCase() : (item.text || '').toLowerCase();
+ return text.includes(searchTerm);
+ });
+
+ const sortedFiltered = filtered.sort((a, b) => {
+ const indexA = (isString(a) ? a : a.text || '').toLowerCase().indexOf(searchTerm);
+ const indexB = (isString(b) ? b : b.text || '').toLowerCase().indexOf(searchTerm);
+ return indexA - indexB;
+ });
+
+ return sortedFiltered.filter(item => {
+ const text = isString(item) ? item : item.text || '';
+ const textLower = text.toLowerCase();
+ const englishChars = getEnglishCharacters(textLower);
+ return should_filter_by_char
+ ? matchStringByChar(text, val)
+ : englishChars.includes(val) || textLower.includes(val);
+ });
+};
+const Autocomplete = React.memo((props: TAutocompleteProps) => {
+ const NO_SEARCH_RESULT = getSearchNotFoundOption();
+ const {
+ autoComplete,
+ className,
+ data_testid,
+ dropdown_offset,
+ error,
+ has_updating_list = true,
+ hide_list = false,
+ historyValue,
+ input_id,
+ is_alignment_top,
+ is_list_visible = false,
+ list_items,
+ list_portal_id,
+ not_found_text = NO_SEARCH_RESULT,
+ onHideDropdownList,
+ onItemSelection,
+ onSearch,
+ onScrollStop,
+ onShowDropdownList,
+ should_filter_by_char,
+ show_list = false,
+ value,
+ ...other_props
+ } = props;
+
+ const dropdown_ref = React.useRef(null);
+ const list_wrapper_ref = React.useRef(null);
+ const list_item_ref = React.useRef(null);
+ const input_wrapper_ref = React.useRef(null);
+
+ const [should_show_list, setShouldShowList] = React.useState(false);
+ const [input_value, setInputValue] = React.useState('');
+ const [active_index, setActiveIndex] = React.useState(-1);
+ const [filtered_items, setFilteredItems] = React.useState(list_items);
+ const [style, setStyle] = React.useState({});
+ useBlockScroll(list_portal_id && should_show_list ? input_wrapper_ref : undefined);
+
+ let scroll_timeout: ReturnType | undefined;
+ let scroll_top_position = null;
+
+ React.useEffect(() => {
+ if (has_updating_list) {
+ let new_filtered_items = [];
+
+ if (is_list_visible) {
+ if (typeof onSearch === 'function') {
+ new_filtered_items = onSearch(value.toLowerCase(), list_items);
+ } else {
+ new_filtered_items = getFilteredItems(value.toLowerCase(), list_items);
+ }
+ } else {
+ new_filtered_items = list_items;
+ }
+
+ setFilteredItems(new_filtered_items);
+ if (historyValue) {
+ const index = new_filtered_items.findIndex(
+ object => (typeof object === 'object' ? object.text : object) === historyValue
+ );
+ setInputValue(historyValue);
+ setActiveIndex(index);
+ } else {
+ setInputValue('');
+ setActiveIndex(-1);
+ }
+ }
+ }, [list_items, has_updating_list, historyValue]);
+
+ React.useEffect(() => {
+ if (is_list_visible) {
+ const index = filtered_items.findIndex(
+ item => (typeof item === 'object' ? item.text : item) === historyValue
+ );
+ setActiveIndex(index);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filtered_items]);
+
+ React.useEffect(() => {
+ if (show_list) showDropdownList();
+ if (hide_list) hideDropdownList();
+ if (should_show_list && list_item_ref.current) {
+ const item = list_item_ref.current.offsetTop;
+ dropdown_ref.current?.scrollTo({ top: item, behavior: 'smooth' });
+ }
+ }, [show_list, hide_list, should_show_list, list_item_ref]);
+
+ React.useEffect(() => {
+ if (list_wrapper_ref.current && list_portal_id && should_show_list) {
+ const position_style = getPosition({
+ preferred_alignment: is_alignment_top ? 'top' : 'bottom',
+ parent_el: input_wrapper_ref.current,
+ child_el: list_wrapper_ref.current,
+ });
+
+ setStyle(position_style.style);
+ }
+ }, [should_show_list, is_alignment_top, list_portal_id, filtered_items?.length]);
+
+ const handleScrollStop = (e: React.UIEvent) => {
+ // pass onScrollStop func callback when scrolling stops
+ if (!onScrollStop) return;
+
+ const element = e.currentTarget;
+ scroll_top_position = element.scrollTop;
+ if (scroll_top_position === element.scrollTop) {
+ clearTimeout(scroll_timeout);
+ }
+ scroll_timeout = setTimeout(() => {
+ onScrollStop?.();
+ }, 150);
+ };
+
+ const onKeyPressed = (event: React.KeyboardEvent) => {
+ switch (event.keyCode) {
+ case KEY_CODE.ENTER:
+ event.preventDefault();
+ hideDropdownList();
+ onSelectItem(filtered_items[active_index]);
+ break;
+ case KEY_CODE.TAB:
+ if (should_show_list) {
+ hideDropdownList();
+ onSelectItem(filtered_items[active_index]);
+ }
+ break;
+ case KEY_CODE.ESCAPE:
+ event.preventDefault();
+ hideDropdownList();
+ break;
+ case KEY_CODE.KEYDOWN:
+ if (!should_show_list) showDropdownList();
+ setActiveDown();
+ break;
+ case KEY_CODE.KEYUP:
+ if (!should_show_list) showDropdownList();
+ else setActiveUp();
+ break;
+ default:
+ if (!should_show_list) showDropdownList();
+ break;
+ }
+ };
+
+ const setActiveUp = () => {
+ if (active_index !== -1) {
+ const up = active_index - 1;
+ const should_scroll_to_last = up < 0;
+
+ if (should_scroll_to_last) {
+ const list_height = dropdown_ref.current?.clientHeight;
+ setActiveIndex(filtered_items.length - 1);
+ dropdown_ref.current?.scrollTo({ top: list_height, behavior: 'smooth' });
+ } else {
+ const item_height = Number(list_item_ref.current?.getBoundingClientRect().height);
+ const item_top = Math.floor(Number(list_item_ref.current?.getBoundingClientRect()?.top)) - item_height;
+
+ if (!isListItemWithinView(item_top)) {
+ const top_of_list = Number(list_item_ref.current?.offsetTop) - item_height;
+ dropdown_ref.current?.scrollTo({ top: top_of_list, behavior: 'smooth' });
+ }
+ setActiveIndex(up);
+ }
+ }
+ };
+
+ const isListItemWithinView = (item_top: number) => {
+ const list_height = Number(dropdown_ref.current?.clientHeight);
+ const wrapper_top = Math.floor(Number(list_wrapper_ref.current?.getBoundingClientRect().top));
+ const wrapper_bottom = Math.floor(Number(list_wrapper_ref.current?.getBoundingClientRect().top)) + list_height;
+
+ if (item_top >= wrapper_bottom) return false;
+ return item_top > wrapper_top;
+ };
+
+ const setActiveDown = () => {
+ if (active_index === -1 || !list_item_ref.current) {
+ setActiveIndex(0);
+ } else if (typeof active_index === 'number') {
+ const down = active_index + 1;
+ const should_scroll_to_first = down >= filtered_items.length;
+
+ if (should_scroll_to_first) {
+ setActiveIndex(0);
+ dropdown_ref.current?.scrollTo({ top: 0, behavior: 'smooth' });
+ } else {
+ const item_height = list_item_ref.current.getBoundingClientRect().height;
+ const item_top =
+ Math.floor(list_item_ref.current.getBoundingClientRect().top) + item_height + item_height / 2;
+ const list_height = Number(dropdown_ref.current?.clientHeight);
+
+ if (!isListItemWithinView(item_top)) {
+ const items_above = list_height / item_height - 2;
+ const bottom_of_list = list_item_ref.current.offsetTop - items_above * item_height;
+ dropdown_ref.current?.scrollTo?.({ top: bottom_of_list, behavior: 'smooth' });
+ }
+ setActiveIndex(down);
+ }
+ }
+ };
+
+ const onBlur = (e: React.FocusEvent) => {
+ e.preventDefault();
+ hideDropdownList();
+
+ if (!is_list_visible) setFilteredItems(list_items);
+
+ if (input_value === '') {
+ onItemSelection?.({
+ text: not_found_text,
+ value: '',
+ });
+ }
+ if (typeof other_props.onBlur === 'function') {
+ other_props.onBlur(e);
+ }
+ };
+
+ const onSelectItem = (item: TItem) => {
+ if (!item) return;
+
+ setInputValue((typeof item === 'object' ? item.text : item) || '');
+
+ onItemSelection?.(item);
+ };
+
+ const showDropdownList = () => {
+ setShouldShowList(true);
+
+ onShowDropdownList?.();
+ };
+
+ const hideDropdownList = () => {
+ setShouldShowList(false);
+
+ onHideDropdownList?.();
+ };
+
+ const filterList = (e: React.FormEvent) => {
+ const val = (e.target as HTMLInputElement).value.toLowerCase();
+ let new_filtered_items = [];
+
+ if (typeof onSearch === 'function') {
+ new_filtered_items = onSearch(val, list_items);
+ } else {
+ new_filtered_items = getFilteredItems(val, list_items, should_filter_by_char);
+ }
+
+ if (!new_filtered_items.length) {
+ setInputValue('');
+ }
+ setFilteredItems(new_filtered_items);
+ };
+
+ return (
+
+
+ {
+ if (should_show_list) hideDropdownList();
+ else showDropdownList();
+ }}
+ // Field's onBlur still needs to run to perform form functions such as validation
+ onBlur={onBlur}
+ value={
+ // This allows us to let control of value externally (from ) or internally if used without form
+ typeof onItemSelection === 'function' ? value : input_value
+ }
+ trailing_icon={
+ other_props.trailing_icon ? (
+ other_props.trailing_icon
+ ) : (
+
+ )
+ }
+ />
+
+
+
+ );
+});
+
+Autocomplete.displayName = 'Autocomplete';
+
+export default Autocomplete;
diff --git a/src/components/shared_ui/autocomplete/index.ts b/src/components/shared_ui/autocomplete/index.ts
new file mode 100644
index 00000000..52a9f08b
--- /dev/null
+++ b/src/components/shared_ui/autocomplete/index.ts
@@ -0,0 +1,5 @@
+import Autocomplete from './autocomplete';
+
+import './autocomplete.scss';
+
+export default Autocomplete;
diff --git a/src/components/shared_ui/checkbox/checkbox.scss b/src/components/shared_ui/checkbox/checkbox.scss
new file mode 100644
index 00000000..4c3f10a3
--- /dev/null
+++ b/src/components/shared_ui/checkbox/checkbox.scss
@@ -0,0 +1,48 @@
+.dc-checkbox {
+ display: flex;
+ justify-content: flex-start;
+ cursor: pointer;
+ align-items: center;
+ user-select: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+
+ &__input {
+ display: none;
+ }
+ &__box {
+ display: flex;
+ width: 16px;
+ height: 16px;
+ transition: 0.3s ease-in-out;
+ flex-shrink: 0;
+ margin: 0 8px;
+ border-radius: 2px;
+ align-self: center;
+ box-sizing: border-box;
+ border: 2px solid var(--text-less-prominent);
+ outline: none !important;
+
+ &--active {
+ border: none;
+ background-color: var(--brand-red-coral);
+ }
+ }
+ &--active {
+ border: none;
+ background-color: var(--brand-red-coral);
+ }
+ &--disabled {
+ opacity: 0.5;
+ cursor: not-allowed !important;
+ }
+ &--grey-disabled {
+ background-color: var(--checkbox-disabled-grey);
+ }
+
+ &__label {
+ &--error {
+ color: var(--text-loss-danger) !important;
+ }
+ }
+}
diff --git a/src/components/shared_ui/checkbox/checkbox.tsx b/src/components/shared_ui/checkbox/checkbox.tsx
new file mode 100644
index 00000000..b5166922
--- /dev/null
+++ b/src/components/shared_ui/checkbox/checkbox.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import Text from '../text';
+
+type TCheckBoxProps = Omit, 'value' | 'label'> & {
+ className?: string;
+ classNameLabel?: string;
+ defaultChecked?: boolean;
+ disabled?: boolean;
+ greyDisabled?: boolean;
+ id?: string;
+ label: string | React.ReactElement;
+ label_font_size?: string;
+ onChange?: (e: React.ChangeEvent | React.KeyboardEvent) => void;
+ value?: boolean;
+ withTabIndex?: number;
+ has_error?: boolean;
+ label_line_height?: string;
+};
+
+const Checkbox = React.forwardRef(
+ (
+ {
+ className,
+ classNameLabel,
+ disabled = false,
+ id,
+ label,
+ label_font_size = 'xs',
+ label_line_height = 'unset',
+ defaultChecked,
+ onChange, // This needs to be here so it's not included in `otherProps`
+ value = false,
+ withTabIndex = 0,
+ greyDisabled = false,
+ has_error = false,
+ ...otherProps
+ },
+ ref
+ ) => {
+ const [checked, setChecked] = React.useState(defaultChecked || value);
+ React.useEffect(() => {
+ setChecked(defaultChecked || value);
+ }, [value, defaultChecked]);
+
+ const onInputChange: React.ChangeEventHandler = e => {
+ e.persist();
+ setChecked(!checked);
+ onChange?.(e);
+ };
+
+ const handleKeyDown: React.KeyboardEventHandler = e => {
+ // Enter or space
+ if (!disabled && (e.key === 'Enter' || e.keyCode === 32)) {
+ onChange?.(e);
+ setChecked(!checked);
+ }
+ };
+
+ return (
+ e.stopPropagation()}
+ className={classNames('dc-checkbox', className, {
+ 'dc-checkbox--disabled': disabled,
+ })}
+ >
+
+
+
+ {!!checked && }
+
+
+
+ {label}
+
+
+ );
+ }
+);
+
+Checkbox.displayName = 'Checkbox';
+
+export default Checkbox;
diff --git a/src/components/shared_ui/checkbox/index.ts b/src/components/shared_ui/checkbox/index.ts
new file mode 100644
index 00000000..74b83986
--- /dev/null
+++ b/src/components/shared_ui/checkbox/index.ts
@@ -0,0 +1,5 @@
+import Checkbox from './checkbox';
+
+import './checkbox.scss';
+
+export default Checkbox;
diff --git a/src/components/shared_ui/circular-progress/circular-progress.scss b/src/components/shared_ui/circular-progress/circular-progress.scss
new file mode 100644
index 00000000..ca068d89
--- /dev/null
+++ b/src/components/shared_ui/circular-progress/circular-progress.scss
@@ -0,0 +1,30 @@
+.dc-circular-progress {
+ position: relative;
+ line-height: 0;
+ width: fit-content;
+
+ &__bar {
+ transform: scaleX(-1) rotate(-90deg);
+ transform-origin: 50% 50%;
+ transition: stroke-dashoffset 1s;
+ stroke: var(--brand-secondary);
+
+ &--warning {
+ stroke: var(--status-warning);
+ }
+ &--danger {
+ stroke: var(--status-danger);
+ }
+ }
+ &--clockwise {
+ transform: rotate(-90deg);
+ }
+ &__icon {
+ position: absolute;
+ width: 1.6rem;
+ height: 100%;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ }
+}
diff --git a/src/components/shared_ui/circular-progress/circular-progress.tsx b/src/components/shared_ui/circular-progress/circular-progress.tsx
new file mode 100644
index 00000000..9785bad3
--- /dev/null
+++ b/src/components/shared_ui/circular-progress/circular-progress.tsx
@@ -0,0 +1,52 @@
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+type TCircularProgressProps = {
+ className?: string;
+ danger_limit?: number;
+ is_clockwise?: boolean;
+ progress?: number;
+ radius?: number;
+ stroke?: number;
+ warning_limit?: number;
+ icon?: string;
+};
+
+const CircularProgress = ({
+ className,
+ danger_limit = 20,
+ icon = '',
+ is_clockwise = false,
+ progress = 0,
+ radius = 22,
+ stroke = 3,
+ warning_limit = 50,
+}: TCircularProgressProps) => {
+ const normalizedRadius = radius - stroke / 2;
+ const circumference = normalizedRadius * 2 * Math.PI;
+ const strokeDashoffset = circumference - (progress / 100) * circumference;
+ return (
+
+
+
+ danger_limit,
+ 'dc-circular-progress__bar--danger': progress <= danger_limit,
+ })}
+ cx={radius}
+ cy={radius}
+ fill='transparent'
+ r={normalizedRadius}
+ strokeDasharray={`${circumference} ${circumference}`}
+ strokeWidth={stroke}
+ style={{ strokeDashoffset }}
+ />
+
+
+ );
+};
+
+export default CircularProgress;
diff --git a/src/components/shared_ui/circular-progress/index.ts b/src/components/shared_ui/circular-progress/index.ts
new file mode 100644
index 00000000..73a10587
--- /dev/null
+++ b/src/components/shared_ui/circular-progress/index.ts
@@ -0,0 +1,5 @@
+import CircularProgress from './circular-progress';
+
+import './circular-progress.scss';
+
+export default CircularProgress;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/accumulator-card-body.tsx b/src/components/shared_ui/contract-card/contract-card-items/accumulator-card-body.tsx
new file mode 100644
index 00000000..c650f1fc
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/accumulator-card-body.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { ContractUpdate } from '@deriv/api-types';
+
+import { getLimitOrderAmount, isCryptocurrency, isValidToSell } from '@/components/shared';
+
+import ArrowIndicator from '../../arrow-indicator';
+import MobileWrapper from '../../mobile-wrapper';
+import Money from '../../money';
+import { TGetCardLables } from '../../types/common.types';
+import { TToastConfig } from '../../types/contract.types';
+import { ResultStatusIcon } from '../result-overlay/result-overlay';
+
+import ContractCardItem from './contract-card-item';
+import ToggleCardDialog from './toggle-card-dialog';
+
+type TAccumulatorCardBody = {
+ addToast: (toast_config: TToastConfig) => void;
+ contract_info: TContractInfo;
+ contract_update?: ContractUpdate;
+ currency: Required['currency'];
+ current_focus?: string | null;
+ error_message_alignment?: string;
+ getCardLabels: TGetCardLables;
+ getContractById: React.ComponentProps['getContractById'];
+ indicative?: number;
+ is_sold: boolean;
+ onMouseLeave?: () => void;
+ removeToast: (toast_id: string) => void;
+ setCurrentFocus: (value: string | null) => void;
+ totalProfit: number;
+ is_positions?: boolean;
+};
+
+const AccumulatorCardBody = ({
+ contract_info,
+ contract_update,
+ currency,
+ getCardLabels,
+ indicative,
+ is_sold,
+ is_positions,
+ ...toggle_card_dialog_props
+}: TAccumulatorCardBody) => {
+ const { buy_price, profit, limit_order, sell_price } = contract_info;
+ const { take_profit } = getLimitOrderAmount(contract_update || limit_order);
+ const is_valid_to_sell = isValidToSell(contract_info);
+ const { CONTRACT_VALUE, STAKE, TAKE_PROFIT, TOTAL_PROFIT_LOSS } = getCardLabels();
+ let is_won, is_loss;
+ if (profit) {
+ is_won = +profit > 0;
+ is_loss = +profit < 0;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {!is_sold && (
+
+ )}
+
+
+
+ {!is_sold && }
+
+
+ {take_profit ? : - }
+ {is_valid_to_sell && (
+
+ )}
+
+
+ {!!is_sold && (
+
+
+
+
+
+ )}
+
+ );
+};
+
+export default React.memo(AccumulatorCardBody);
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-card-body.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-card-body.tsx
new file mode 100644
index 00000000..447b8544
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-card-body.tsx
@@ -0,0 +1,224 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import {
+ getCurrentTick,
+ getDisplayStatus,
+ getIndicativePrice,
+ getTotalProfit,
+ isCryptocurrency,
+} from '@/components/shared';
+
+import ArrowIndicator from '../../arrow-indicator';
+import CurrencyBadge from '../../currency-badge';
+import DesktopWrapper from '../../desktop-wrapper';
+import MobileWrapper from '../../mobile-wrapper';
+import Money from '../../money';
+import ProgressSliderMobile from '../../progress-slider-mobile';
+import { ResultStatusIcon } from '../result-overlay/result-overlay';
+
+import AccumulatorCardBody from './accumulator-card-body';
+import ContractCardItem from './contract-card-item';
+import { TGeneralContractCardBodyProps } from './contract-update-form';
+import LookBacksCardBody from './lookbacks-card-body';
+import MultiplierCardBody from './multiplier-card-body';
+import TurbosCardBody from './turbos-card-body';
+import VanillaOptionsCardBody from './vanilla-options-card-body';
+
+export type TContractCardBodyProps = {
+ is_accumulator?: boolean;
+ is_lookbacks?: boolean;
+ is_multiplier: boolean;
+ is_turbos?: boolean;
+ is_vanilla?: boolean;
+ server_time: moment.Moment;
+} & TGeneralContractCardBodyProps;
+
+const ContractCardBody = ({
+ addToast,
+ contract_info,
+ contract_update,
+ currency,
+ current_focus,
+ error_message_alignment,
+ getCardLabels,
+ getContractById,
+ has_progress_slider,
+ is_accumulator,
+ is_mobile,
+ is_multiplier,
+ is_positions,
+ is_sold,
+ is_turbos,
+ is_vanilla,
+ is_lookbacks,
+ onMouseLeave,
+ removeToast,
+ server_time,
+ setCurrentFocus,
+ should_show_cancellation_warning,
+ toggleCancellationWarning,
+}: TContractCardBodyProps) => {
+ const indicative = getIndicativePrice(contract_info);
+ const { buy_price, sell_price, payout, profit, tick_count, date_expiry, purchase_time } = contract_info;
+ const current_tick = tick_count ? getCurrentTick(contract_info) : null;
+ const { CONTRACT_VALUE, POTENTIAL_PAYOUT, TOTAL_PROFIT_LOSS, STAKE } = getCardLabels();
+
+ const progress_slider_mobile_el = (
+
+ );
+
+ const toggle_card_dialog_props = {
+ addToast,
+ current_focus,
+ error_message_alignment,
+ getContractById,
+ onMouseLeave,
+ removeToast,
+ setCurrentFocus,
+ totalProfit: is_multiplier && !isNaN(Number(profit)) ? getTotalProfit(contract_info) : Number(profit),
+ };
+
+ let card_body;
+
+ if (is_multiplier) {
+ card_body = (
+
+ );
+ } else if (is_accumulator) {
+ card_body = (
+
+ );
+ } else if (is_turbos) {
+ card_body = (
+
+ );
+ } else if (is_vanilla) {
+ card_body = (
+
+ );
+ } else if (is_lookbacks) {
+ card_body = (
+
+ );
+ } else {
+ card_body = (
+
+
+
0}
+ >
+
+ {!is_sold && (
+
+ )}
+
+
+ 0,
+ 'dc-contract-card--loss': Number(profit) < 0,
+ })}
+ >
+
+
+ {!is_sold && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {is_sold ? (
+
+ ) : (
+ progress_slider_mobile_el
+ )}
+
+
+
+ );
+ }
+
+ return (
+
+
+ {card_body}
+
+
+ {card_body}
+
+
+
+ );
+};
+
+export default ContractCardBody;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-card-dialog.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-card-dialog.tsx
new file mode 100644
index 00000000..50c6f372
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-card-dialog.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { CSSTransition } from 'react-transition-group';
+
+import { useOnClickOutside } from '@/hooks/useOnClickOutside';
+
+import './sass/contract-card-dialog.scss';
+
+export type TContractCardDialogProps = {
+ children: React.ReactNode;
+ is_visible: boolean;
+ left: number;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ toggleDialog: (e: any) => void; // This function accomodates events for various HTML elements, which have no overlap, so typing it to any
+ toggle_ref?: React.RefObject;
+ top: number;
+};
+
+const ContractCardDialog = React.forwardRef(
+ (
+ { children, is_visible, left, toggleDialog, toggle_ref, top }: TContractCardDialogProps,
+ ref: React.ForwardedRef
+ ) => {
+ const validateClickOutside = (event: MouseEvent) =>
+ is_visible && !toggle_ref?.current?.contains(event.target as Node);
+
+ useOnClickOutside(ref as React.RefObject, toggleDialog, validateClickOutside);
+
+ const dialog = (
+
+
+ {children}
+
+
+ );
+ const deriv_app_element = document.getElementById('deriv_app');
+ return ReactDOM.createPortal(
+ dialog, // use portal to render dialog above ThemedScrollbars container
+ deriv_app_element || document.body
+ );
+ }
+);
+
+ContractCardDialog.displayName = 'ContractCardDialog';
+
+export default ContractCardDialog;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-card-footer.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-card-footer.tsx
new file mode 100644
index 00000000..9b10cbb9
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-card-footer.tsx
@@ -0,0 +1,120 @@
+import React from 'react';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+
+import { hasContractEntered, isOpen, isValidToCancel, isValidToSell, useNewRowTransition } from '@/components/shared';
+import { TContractInfo } from '@/components/shared/src/utils/contract/contract-types';
+import { Localize } from '@/utils/tmp/dummy';
+
+import Text from '../../text';
+import { TGetCardLables } from '../../types';
+
+import ContractCardSell from './contract-card-sell';
+import MultiplierCloseActions from './multiplier-close-actions';
+
+export type TCardFooterPropTypes = {
+ contract_info: TContractInfo;
+ getCardLabels: TGetCardLables;
+ is_multiplier?: boolean;
+ is_positions?: boolean;
+ is_sell_requested: boolean;
+ is_lookbacks?: boolean;
+ onClickCancel: (contract_id?: number) => void;
+ onClickSell: (contract_id?: number) => void;
+ onFooterEntered?: () => void;
+ server_time: moment.Moment;
+ should_show_transition?: boolean;
+};
+
+const CardFooter = ({
+ contract_info,
+ getCardLabels,
+ is_multiplier,
+ is_positions,
+ is_sell_requested,
+ is_lookbacks,
+ onClickCancel,
+ onClickSell,
+ onFooterEntered,
+ server_time,
+ should_show_transition,
+}: TCardFooterPropTypes) => {
+ const { in_prop } = useNewRowTransition(!!should_show_transition);
+
+ const is_valid_to_cancel = isValidToCancel(contract_info);
+
+ const should_show_sell = hasContractEntered(contract_info) && isOpen(contract_info);
+ const should_show_sell_note = is_lookbacks && isValidToSell(contract_info) && should_show_sell;
+
+ if (!should_show_sell) return null;
+
+ return (
+
+
+ {is_multiplier ? (
+
+
+
+ ) : (
+
+
+
+
+ {should_show_sell_note && (
+
+ ]}
+ />
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default CardFooter;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-card-header.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-card-header.tsx
new file mode 100644
index 00000000..fb3b9155
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-card-header.tsx
@@ -0,0 +1,207 @@
+import React from 'react';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+
+import { Button } from '@deriv-com/ui';
+
+import {
+ getCurrentTick,
+ getGrowthRatePercentage,
+ getLocalizedTurbosSubtype,
+ isAccumulatorContract,
+ isBot,
+ isHighLow,
+ isMobile,
+ isMultiplierContract,
+ isSmartTraderContract,
+ isTurbosContract,
+} from '@/components/shared';
+import { TContractInfo } from '@/components/shared/src/utils/contract/contract-types';
+import { Icon } from '@/utils/tmp/dummy';
+
+import DesktopWrapper from '../../desktop-wrapper';
+import MobileWrapper from '../../mobile-wrapper';
+import ProgressSlider from '../../progress-slider';
+import Text from '../../text';
+import { TGetCardLables, TGetContractTypeDisplay } from '../../types/common.types';
+
+import ContractTypeCell from './contract-type-cell';
+import TickCounterBar from './tick-counter-bar';
+
+export type TContractCardHeaderProps = {
+ contract_info: TContractInfo;
+ display_name: string;
+ getCardLabels: TGetCardLables;
+ getContractTypeDisplay: TGetContractTypeDisplay;
+ has_progress_slider: boolean;
+ is_mobile: boolean;
+ is_sell_requested: boolean;
+ is_valid_to_sell?: boolean;
+ onClickSell: (contract_id?: number) => void;
+ server_time: moment.Moment;
+ id?: number;
+ is_sold?: boolean;
+};
+
+const ContractCardHeader = ({
+ contract_info,
+ display_name,
+ getCardLabels,
+ getContractTypeDisplay,
+ has_progress_slider,
+ id,
+ is_sell_requested,
+ is_sold: is_contract_sold,
+ is_valid_to_sell,
+ onClickSell,
+ server_time,
+}: TContractCardHeaderProps) => {
+ const current_tick = contract_info.tick_count ? getCurrentTick(contract_info) : null;
+ const {
+ growth_rate,
+ underlying,
+ multiplier,
+ contract_type,
+ shortcode,
+ purchase_time,
+ date_expiry,
+ tick_count,
+ tick_passed,
+ } = contract_info;
+ const is_bot = isBot();
+ const is_sold = !!contract_info.is_sold || is_contract_sold;
+ const is_accumulator = isAccumulatorContract(contract_type);
+ const is_smarttrader_contract = isSmartTraderContract(contract_type);
+ const is_mobile = isMobile();
+ const is_turbos = isTurbosContract(contract_type);
+ const is_multipliers = isMultiplierContract(contract_type);
+ const is_high_low = isHighLow({ shortcode });
+
+ const contract_type_list_info = React.useMemo(
+ () => [
+ {
+ is_param_displayed: is_multipliers,
+ displayed_param: `${getContractTypeDisplay(contract_type ?? '', {
+ isHighLow: is_high_low,
+ })} x${multiplier}`.trim(),
+ },
+ {
+ is_param_displayed: is_accumulator,
+ displayed_param: `${getGrowthRatePercentage(growth_rate || 0)}%`,
+ },
+ {
+ is_param_displayed: is_turbos,
+ displayed_param: getLocalizedTurbosSubtype(contract_type),
+ },
+ ],
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [contract_type, growth_rate, multiplier, is_accumulator, is_multipliers, is_turbos, is_high_low]
+ );
+
+ const displayed_trade_param =
+ contract_type_list_info.find(contract_type_item_info => contract_type_item_info.is_param_displayed)
+ ?.displayed_param || '';
+
+ return (
+
+
+
+
+
+ {display_name || contract_info.display_name}
+
+
+
+
+
+
+ {is_valid_to_sell ? (
+
+
+ onClickSell(id)}
+ variant='outlined'
+ />
+
+
+ ) : null}
+
+
+ {!is_sold && is_accumulator && (
+
+ )}
+
+
+
+
+ {(!has_progress_slider || !!is_sold) &&
}
+ {has_progress_slider && !is_sold && !is_accumulator && (
+
+ )}
+
+
+ );
+};
+
+export default ContractCardHeader;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-card-item.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-card-item.tsx
new file mode 100644
index 00000000..9f16cea3
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-card-item.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import classNames from 'classnames';
+
+export type TContractCardItemProps = {
+ className: string;
+ header: string;
+ is_crypto: boolean;
+ is_loss: boolean;
+ is_won: boolean;
+};
+
+const ContractCardItem = ({
+ className,
+ children,
+ header,
+ is_crypto,
+ is_loss,
+ is_won,
+}: React.PropsWithChildren>) => {
+ return (
+
+
{header}
+
+ {children}
+
+
+ );
+};
+
+export default ContractCardItem;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-card-sell.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-card-sell.tsx
new file mode 100644
index 00000000..42a6cb0a
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-card-sell.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Button } from '@deriv-com/ui';
+
+import { hasContractEntered, isForwardStarting, isOpen, isValidToSell } from '@/components/shared';
+import { TContractInfo } from '@/components/shared/src/utils/contract/contract-types';
+
+import { TGetCardLables } from '../../types';
+
+export type TContractCardSellProps = {
+ contract_info: TContractInfo;
+ getCardLabels: TGetCardLables;
+ is_sell_requested: boolean;
+ onClickSell?: (contract_id?: number) => void;
+};
+
+const ContractCardSell = ({ contract_info, getCardLabels, is_sell_requested, onClickSell }: TContractCardSellProps) => {
+ const is_valid_to_sell = isValidToSell(contract_info);
+ const should_show_sell =
+ (hasContractEntered(contract_info) ||
+ isForwardStarting(contract_info?.shortcode ?? '', contract_info.purchase_time)) &&
+ isOpen(contract_info);
+
+ const onClick = (ev: React.MouseEvent) => {
+ onClickSell?.(contract_info.contract_id);
+ ev.stopPropagation();
+ ev.preventDefault();
+ };
+
+ return should_show_sell ? (
+
+ {is_valid_to_sell ? (
+
+ ) : (
+ {getCardLabels().RESALE_NOT_OFFERED}
+ )}
+
+ ) : null;
+};
+
+export default ContractCardSell;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-type-cell.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-type-cell.tsx
new file mode 100644
index 00000000..0849a659
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-type-cell.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { isLookBacksContract, isSmartTraderContract, isVanillaContract } from '@/components/shared';
+import { IconTradeTypes } from '@/utils/tmp/dummy';
+
+import { TGetContractTypeDisplay } from '../../types';
+
+export type TContractTypeCellProps = {
+ getContractTypeDisplay: TGetContractTypeDisplay;
+ is_high_low: boolean;
+ is_multipliers?: boolean;
+ is_turbos?: boolean;
+ type?: string;
+ displayed_trade_param?: React.ReactNode;
+};
+
+const ContractTypeCell = ({
+ displayed_trade_param,
+ getContractTypeDisplay,
+ is_high_low,
+ is_multipliers,
+ is_turbos,
+ type = '',
+}: TContractTypeCellProps) => (
+
+
+
+
+
+
+ {getContractTypeDisplay(type, { isHighLow: is_high_low, showMainTitle: is_multipliers || is_turbos }) ||
+ ''}
+
+ {displayed_trade_param && (
+
{displayed_trade_param}
+ )}
+
+
+);
+
+export default ContractTypeCell;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/contract-update-form.tsx b/src/components/shared_ui/contract-card/contract-card-items/contract-update-form.tsx
new file mode 100644
index 00000000..52f72a39
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/contract-update-form.tsx
@@ -0,0 +1,265 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Button } from '@deriv-com/ui';
+
+import {
+ getCancellationPrice,
+ getContractUpdateConfig,
+ getLimitOrderAmount,
+ isCryptocurrency,
+ isDeepEqual,
+ isMultiplierContract,
+ pick,
+} from '@/components/shared';
+import { TContractInfo, TContractStore } from '@/components/shared/contract/contract-types';
+import { Localize } from '@/utils/tmp/dummy';
+
+import ArrowIndicator from '../../arrow-indicator';
+import InputWithCheckbox from '../../input-wth-checkbox';
+import MobileWrapper from '../../mobile-wrapper';
+import Money from '../../money';
+import Text from '../../text';
+import { TGetCardLables, TToastConfig } from '../../types';
+
+export type TGeneralContractCardBodyProps = {
+ addToast: (toast_config: TToastConfig) => void;
+ contract_info: TContractInfo;
+ contract_update: TContractInfo['contract_update'];
+ currency: string;
+ current_focus?: string | null;
+ error_message_alignment?: string;
+ getCardLabels: TGetCardLables;
+ getContractById: (contract_id: number) => TContractStore;
+ should_show_cancellation_warning: boolean;
+ has_progress_slider: boolean;
+ is_mobile: boolean;
+ is_sold: boolean;
+ onMouseLeave?: () => void;
+ removeToast: (toast_id: string) => void;
+ setCurrentFocus: (name: string | null) => void;
+ toggleCancellationWarning: (state_change?: boolean) => void;
+ progress_slider?: React.ReactNode;
+ is_positions?: boolean;
+};
+export type TContractUpdateFormProps = Pick<
+ TGeneralContractCardBodyProps,
+ | 'addToast'
+ | 'current_focus'
+ | 'error_message_alignment'
+ | 'getCardLabels'
+ | 'onMouseLeave'
+ | 'removeToast'
+ | 'setCurrentFocus'
+> & {
+ contract: TContractStore;
+ error_message_alignment?: string;
+ getCardLabels: TGetCardLables;
+ onMouseLeave?: () => void;
+ removeToast: (toast_id: string) => void;
+ setCurrentFocus: (name: string | null) => void;
+ toggleDialog: (e: React.MouseEvent) => void;
+ getContractById: (contract_id: number) => TContractStore;
+ is_accumulator?: boolean;
+ isMobile?: boolean;
+ is_turbos?: boolean;
+ totalProfit: number;
+};
+
+const ContractUpdateForm = (props: TContractUpdateFormProps) => {
+ const {
+ addToast,
+ contract,
+ current_focus,
+ error_message_alignment,
+ getCardLabels,
+ isMobile,
+ is_turbos,
+ is_accumulator,
+ onMouseLeave,
+ removeToast,
+ setCurrentFocus,
+ toggleDialog,
+ totalProfit,
+ } = props;
+
+ React.useEffect(() => {
+ return () => contract.clearContractUpdateConfigValues();
+ }, [contract]);
+
+ const {
+ contract_info,
+ contract_update_take_profit,
+ has_contract_update_take_profit,
+ contract_update_stop_loss,
+ has_contract_update_stop_loss,
+ updateLimitOrder,
+ validation_errors,
+ } = contract;
+
+ const [contract_profit_or_loss, setContractProfitOrLoss] = React.useState({
+ contract_update_take_profit,
+ contract_update_stop_loss,
+ });
+
+ const { buy_price, currency = '', is_valid_to_cancel, is_sold } = contract_info;
+ const { stop_loss, take_profit } = getLimitOrderAmount(contract_info.limit_order);
+ const { contract_update_stop_loss: stop_loss_error, contract_update_take_profit: take_profit_error } =
+ validation_errors;
+ const error_messages: Record = {
+ take_profit: has_contract_update_take_profit ? take_profit_error : undefined,
+ stop_loss: has_contract_update_stop_loss ? stop_loss_error : undefined,
+ };
+ const has_validation_errors = Object.keys(error_messages).some(field => error_messages[field]?.length);
+
+ const isValid = (val?: number | null) => !(val === undefined || val === null);
+
+ const is_multiplier = isMultiplierContract(contract_info.contract_type || '');
+ const is_take_profit_valid = has_contract_update_take_profit
+ ? +contract_update_take_profit > 0
+ : isValid(is_multiplier ? stop_loss : take_profit);
+ const is_stop_loss_valid = has_contract_update_stop_loss ? +contract_update_stop_loss > 0 : isValid(take_profit);
+ const is_valid_multiplier_contract_update = is_valid_to_cancel
+ ? false
+ : !!(is_take_profit_valid || is_stop_loss_valid);
+ const is_valid_contract_update = is_multiplier ? is_valid_multiplier_contract_update : !!is_take_profit_valid;
+
+ const getStateToCompare = (_state: Partial) => {
+ const props_to_pick = [
+ 'has_contract_update_take_profit',
+ 'has_contract_update_stop_loss',
+ _state.has_contract_update_take_profit && 'contract_update_take_profit',
+ _state.has_contract_update_stop_loss && 'contract_update_stop_loss',
+ ];
+
+ return pick(_state, props_to_pick);
+ };
+
+ const isStateUnchanged = () => {
+ return isDeepEqual(
+ getStateToCompare(getContractUpdateConfig(contract_info)),
+ getStateToCompare(props.contract)
+ );
+ };
+
+ const onChange = (
+ e: React.ChangeEvent | { target: { name: string; value: number | string | boolean } }
+ ) => {
+ const { name, value } = e.target;
+ setContractProfitOrLoss({
+ ...contract_profit_or_loss,
+ [name]: value,
+ });
+
+ contract.onChange?.({
+ name,
+ value,
+ });
+ };
+
+ const onClick = (e: React.MouseEvent) => {
+ updateLimitOrder();
+ toggleDialog(e);
+ onMouseLeave?.();
+ };
+
+ const take_profit_input = (
+
+ }
+ />
+ );
+
+ const cancellation_price = getCancellationPrice(contract_info);
+ const stop_loss_input = (
+
+ }
+ />
+ );
+
+ return (
+
+
+
+
+ {getCardLabels().TOTAL_PROFIT_LOSS}
+
+
0,
+ }
+ )}
+ >
+
+ {!is_sold && (
+
+ )}
+
+
+
+
+
{take_profit_input}
+ {is_multiplier &&
{stop_loss_input}
}
+
+
+
+
+
+ );
+};
+
+export default ContractUpdateForm;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/lookbacks-card-body.tsx b/src/components/shared_ui/contract-card/contract-card-items/lookbacks-card-body.tsx
new file mode 100644
index 00000000..1789974f
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/lookbacks-card-body.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+
+import { CONTRACT_TYPES, getCardLabels, getDisplayStatus, isCryptocurrency } from '@/components/shared';
+import { Localize } from '@/utils/tmp/dummy';
+
+import ArrowIndicator from '../../arrow-indicator';
+import MobileWrapper from '../../mobile-wrapper';
+import Money from '../../money';
+import Text from '../../text';
+import { ResultStatusIcon } from '../result-overlay/result-overlay';
+
+import ContractCardItem from './contract-card-item';
+import { TGeneralContractCardBodyProps } from './contract-update-form';
+
+type TLookBacksCardBody = Pick & {
+ progress_slider_mobile_el: React.ReactNode;
+ indicative?: number | null;
+};
+
+const LookBacksCardBody = ({
+ contract_info,
+ currency,
+ is_sold,
+ indicative,
+ progress_slider_mobile_el,
+}: TLookBacksCardBody) => {
+ const { buy_price, contract_type, sell_price, profit, multiplier } = contract_info;
+ const { INDICATIVE_PRICE, MULTIPLIER, PROFIT_LOSS, POTENTIAL_PROFIT_LOSS, PAYOUT, PURCHASE_PRICE } =
+ getCardLabels();
+
+ const getPayoutLimit = (contract_type?: string, multiplier?: number) => {
+ let formula_base: string | React.ReactNode = '';
+ if (contract_type === CONTRACT_TYPES.LB_PUT) {
+ formula_base = ;
+ } else if (contract_type === CONTRACT_TYPES.LB_CALL) {
+ formula_base = ;
+ } else if (contract_type === CONTRACT_TYPES.LB_HIGH_LOW) {
+ formula_base = ;
+ }
+
+ return (
+
+
+ {formula_base}
+
+ );
+ };
+
+ return (
+
+
+
0}
+ >
+
+ {!is_sold && }
+
+
+
+ {!is_sold && (
+
+ )}
+
+
+
+
+
{`x${multiplier}`}
+
+
+ {is_sold ? (
+
+ ) : (
+ progress_slider_mobile_el
+ )}
+
+
+
+
+
+ {getPayoutLimit(contract_type, multiplier)}
+
+
+
+ );
+};
+
+export default React.memo(LookBacksCardBody);
diff --git a/src/components/shared_ui/contract-card/contract-card-items/multiplier-card-body.jsx b/src/components/shared_ui/contract-card/contract-card-items/multiplier-card-body.jsx
new file mode 100644
index 00000000..8d13f945
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/multiplier-card-body.jsx
@@ -0,0 +1,146 @@
+import React from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+
+import {
+ getCancellationPrice,
+ getLimitOrderAmount,
+ getTotalProfit,
+ isCryptocurrency,
+ isValidToCancel,
+ isValidToSell,
+} from '@/components/shared';
+
+import ArrowIndicator from '../../arrow-indicator';
+import Money from '../../money';
+
+import ContractCardItem from './contract-card-item';
+import ToggleCardDialog from './toggle-card-dialog';
+
+const MultiplierCardBody = ({
+ contract_info,
+ contract_update,
+ currency,
+ getCardLabels,
+ has_progress_slider,
+ progress_slider,
+ is_mobile,
+ is_sold,
+ should_show_cancellation_warning,
+ toggleCancellationWarning,
+ ...toggle_card_dialog_props
+}) => {
+ const { buy_price, bid_price, limit_order } = contract_info;
+ const { take_profit, stop_loss } = getLimitOrderAmount(contract_update || limit_order);
+ const cancellation_price = getCancellationPrice(contract_info);
+ const is_valid_to_cancel = isValidToCancel(contract_info);
+ const is_valid_to_sell = isValidToSell(contract_info);
+ const total_profit = getTotalProfit(contract_info);
+ const { CONTRACT_COST, CONTRACT_VALUE, DEAL_CANCEL_FEE, STAKE, STOP_LOSS, TAKE_PROFIT, TOTAL_PROFIT_LOSS } =
+ getCardLabels();
+
+ return (
+
+
+
+
+
+
+ 0,
+ 'dc-contract-card--loss': total_profit < 0,
+ })}
+ >
+
+
+ {!is_sold && (
+
+ )}
+
+
+ {cancellation_price ? (
+
+ ) : (
+ -
+ )}
+
+
+
+
+ {has_progress_slider && is_mobile && !is_sold && (
+
{progress_slider}
+ )}
+
+
+ {take_profit ? : - }
+
+
+ {stop_loss ? (
+
+ -
+
+
+ ) : (
+ -
+ )}
+
+ {(is_valid_to_sell || is_valid_to_cancel) && (
+
+ )}
+
+
+ 0}
+ >
+
+ {!is_sold && }
+
+
+ );
+};
+
+MultiplierCardBody.propTypes = {
+ addToast: PropTypes.func,
+ contract_info: PropTypes.object,
+ contract_update: PropTypes.object,
+ currency: PropTypes.string,
+ current_focus: PropTypes.string,
+ error_message_alignment: PropTypes.string,
+ getCardLabels: PropTypes.func,
+ getContractById: PropTypes.func,
+ is_mobile: PropTypes.bool,
+ is_sold: PropTypes.bool,
+ onMouseLeave: PropTypes.func,
+ progress_slider: PropTypes.node,
+ removeToast: PropTypes.func,
+ setCurrentFocus: PropTypes.func,
+ should_show_cancellation_warning: PropTypes.bool,
+ toggleCancellationWarning: PropTypes.func,
+ totalProfit: PropTypes.number.isRequired,
+ has_progress_slider: PropTypes.bool,
+};
+
+export default React.memo(MultiplierCardBody);
diff --git a/src/components/shared_ui/contract-card/contract-card-items/multiplier-close-actions.tsx b/src/components/shared_ui/contract-card/contract-card-items/multiplier-close-actions.tsx
new file mode 100644
index 00000000..dd6ba301
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/multiplier-close-actions.tsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Button } from '@deriv-com/ui';
+
+import { isValidToCancel } from '@/components/shared';
+import { TContractInfo } from '@/components/shared/src/utils/contract/contract-types';
+
+import RemainingTime from '../../remaining-time';
+import { TGetCardLables } from '../../types';
+
+export type TMultiplierCloseActionsProps = {
+ className?: string;
+ contract_info: TContractInfo;
+ getCardLabels: TGetCardLables;
+ is_sell_requested: boolean;
+ onClickCancel: (contract_id?: number) => void;
+ onClickSell: (contract_id?: number) => void;
+ server_time: moment.Moment;
+};
+
+const MultiplierCloseActions = ({
+ className,
+ contract_info,
+ getCardLabels,
+ is_sell_requested,
+ onClickCancel,
+ onClickSell,
+ server_time,
+}: TMultiplierCloseActionsProps) => {
+ const { contract_id, cancellation: { date_expiry: cancellation_date_expiry } = {}, profit } = contract_info;
+
+ const is_valid_to_cancel = isValidToCancel(contract_info);
+
+ return (
+
+ {
+ onClickSell(contract_id);
+ ev.stopPropagation();
+ ev.preventDefault();
+ }}
+ variant='outlined'
+ />
+ {is_valid_to_cancel && (
+ = 0}
+ onClick={ev => {
+ onClickCancel(contract_id);
+ ev.stopPropagation();
+ ev.preventDefault();
+ }}
+ variant='outlined'
+ >
+ {getCardLabels().CANCEL}
+ {cancellation_date_expiry && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default MultiplierCloseActions;
diff --git a/src/components/shared_ui/contract-card/contract-card-items/sass/contract-card-dialog.scss b/src/components/shared_ui/contract-card/contract-card-items/sass/contract-card-dialog.scss
new file mode 100644
index 00000000..2363ff0b
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/sass/contract-card-dialog.scss
@@ -0,0 +1,158 @@
+/** @define dc-contract-card-dialog; weak */
+.dc-contract-card-dialog {
+ position: fixed;
+ display: grid;
+ background: var(--general-main-2);
+ border-radius: $BORDER_RADIUS;
+ box-shadow: 0 4px 8px 2px var(--shadow-menu);
+ transition:
+ transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
+ opacity 0.25s linear;
+ padding: 1.6rem;
+ width: 240px;
+ z-index: 99;
+
+ &--enter-done {
+ opacity: 1;
+ transform: translate3d(0, 0, 0);
+ }
+
+ &--enter,
+ &--exit {
+ opacity: 0;
+ transform: translate3d(-20px, 0, 0);
+ }
+
+ &__input {
+ @include mobile {
+ border-bottom: 1px solid var(--border-disabled);
+ padding: 1rem 1.6rem;
+
+ .dc-popover {
+ padding: 0.6rem 1rem;
+ }
+ }
+
+ .dc-input-wrapper__input {
+ border: 1px solid var(--border-normal);
+ appearance: none;
+ }
+
+ &--currency {
+ position: absolute;
+ height: 3.2rem;
+ right: 4rem;
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ background: transparent;
+ border-color: transparent;
+ z-index: 2;
+ color: inherit;
+
+ &--symbol {
+ padding-bottom: 0.2rem;
+ }
+
+ &:before {
+ @include typeface(--paragraph-center-normal-black);
+
+ color: inherit;
+ }
+ }
+ }
+
+ &__popover {
+ margin-top: -50%;
+ }
+
+ &__popover-bubble {
+ /* postcss-bem-linter: ignore */
+ .dc-checkbox {
+ margin-top: 0.8rem;
+
+ /* postcss-bem-linter: ignore */
+ &__label {
+ font-size: inherit;
+ }
+ }
+ }
+
+ &__button {
+ margin-top: 0.8rem;
+
+ .dc-btn {
+ width: 100%;
+
+ @include mobile {
+ flex: 1;
+ height: 4rem;
+ margin: 0 1.6rem;
+ }
+ }
+ }
+
+ &__form {
+ display: grid;
+ flex: 1;
+
+ @include mobile {
+ grid-template-rows: auto auto 1fr;
+ margin: 0 0 1.6rem;
+
+ &--no-stop-loss {
+ grid-template-rows: auto 1fr;
+ }
+ }
+
+ @include desktop {
+ grid-gap: 0.8rem;
+ }
+
+ .dc-checkbox__box {
+ margin-left: 0;
+ }
+ }
+
+ &__total-profit {
+ height: 3.6rem;
+ padding: 1.6rem;
+ border-bottom: 1px solid var(--border-disabled);
+ color: var(--text-general);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 1.4rem;
+ }
+}
+
+/** @define dc-contract-card-dialog-toggle; */
+.dc-contract-card-dialog-toggle {
+ all: unset;
+ position: absolute;
+ display: flex;
+ justify-content: end;
+ width: calc(100% + 0.4rem);
+ height: calc(100% + 0.4rem);
+
+ /* rtl:ignore */
+ right: 0.4rem;
+ top: -0.4rem;
+ border: 1px solid var(--general-section-1);
+ border-radius: 4px;
+
+ @include mobile {
+ width: 100%;
+ }
+
+ &__icon {
+ /* rtl:ignore */
+ float: right;
+ margin: 3px;
+ }
+
+ &:hover {
+ cursor: pointer;
+ border-color: var(--button-secondary-default);
+ }
+}
diff --git a/src/components/shared_ui/contract-card/contract-card-items/tick-counter-bar.tsx b/src/components/shared_ui/contract-card/contract-card-items/tick-counter-bar.tsx
new file mode 100644
index 00000000..075f3a24
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/tick-counter-bar.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+
+import Text from '../../text';
+
+type TTickCounterBar = {
+ current_tick?: number;
+ label: string;
+ max_ticks_duration?: number;
+};
+const TickCounterBar = ({ current_tick, label, max_ticks_duration }: TTickCounterBar) => (
+
+
+
+ {`${current_tick}/${max_ticks_duration} ${label}`}
+
+
+
+);
+
+export default React.memo(TickCounterBar);
diff --git a/src/components/shared_ui/contract-card/contract-card-items/toggle-card-dialog.tsx b/src/components/shared_ui/contract-card/contract-card-items/toggle-card-dialog.tsx
new file mode 100644
index 00000000..6aa0460a
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/toggle-card-dialog.tsx
@@ -0,0 +1,180 @@
+/* eslint-disable no-unsafe-optional-chaining */
+import React from 'react';
+
+import { isDesktop, isMobile } from '@/components/shared';
+import { Icon } from '@/utils/tmp/dummy';
+
+import DesktopWrapper from '../../desktop-wrapper';
+import Div100vhContainer from '../../div100vh-container';
+import MobileDialog from '../../mobile-dialog';
+import MobileWrapper from '../../mobile-wrapper';
+import Popover from '../../popover';
+
+import ContractCardDialog from './contract-card-dialog';
+import ContractUpdateForm, { TGeneralContractCardBodyProps } from './contract-update-form';
+
+import './sass/contract-card-dialog.scss';
+
+export type TToggleCardDialogProps = Pick<
+ TGeneralContractCardBodyProps,
+ | 'addToast'
+ | 'current_focus'
+ | 'error_message_alignment'
+ | 'getCardLabels'
+ | 'getContractById'
+ | 'onMouseLeave'
+ | 'removeToast'
+ | 'setCurrentFocus'
+> & {
+ contract_id?: number;
+ is_valid_to_cancel?: boolean;
+ should_show_cancellation_warning?: boolean;
+ toggleCancellationWarning?: () => void;
+ is_accumulator?: boolean;
+ is_turbos?: boolean;
+ totalProfit: number;
+};
+
+const ToggleCardDialog = ({
+ addToast,
+ contract_id,
+ getCardLabels,
+ getContractById,
+ is_valid_to_cancel,
+ should_show_cancellation_warning,
+ toggleCancellationWarning,
+ ...passthrough_props
+}: TToggleCardDialogProps) => {
+ const [is_visible, setIsVisible] = React.useState(false);
+ const [top, setTop] = React.useState(0);
+ const [left, setLeft] = React.useState(0);
+
+ const toggle_ref = React.useRef(null);
+ const dialog_ref = React.useRef(null);
+ const contract = getContractById(Number(contract_id));
+
+ React.useEffect(() => {
+ if (is_visible && toggle_ref?.current && dialog_ref?.current) {
+ const icon_bound = toggle_ref.current.getBoundingClientRect();
+ const target_bound = dialog_ref.current.getBoundingClientRect();
+ const body_bound = document.body.getBoundingClientRect();
+
+ let { top: icon_bound_top } = icon_bound;
+ const { right } = icon_bound;
+
+ if (icon_bound_top + target_bound?.height > body_bound.height) {
+ icon_bound_top -= target_bound?.height - icon_bound.height;
+ }
+
+ if (right + target_bound?.width > body_bound.width) {
+ setLeft(right - target_bound?.width - 16);
+ } else {
+ setLeft(right - 16);
+ }
+ setTop(icon_bound_top);
+ }
+ }, [is_visible]);
+
+ const handleClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ };
+
+ const onPopoverClose = () => {
+ toggleCancellationWarning?.();
+ };
+
+ const toggleDialog = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (isMobile() && should_show_cancellation_warning && is_valid_to_cancel) {
+ addToast({
+ key: 'deal_cancellation_active',
+ content: getCardLabels().TAKE_PROFIT_LOSS_NOT_AVAILABLE,
+ type: 'error',
+ });
+ }
+
+ if (is_valid_to_cancel) return;
+
+ setIsVisible(!is_visible);
+ };
+
+ const toggleDialogWrapper = React.useCallback(toggleDialog, [toggleDialog]);
+
+ const edit_icon = (
+
+ );
+
+ return (
+
+ {is_valid_to_cancel && should_show_cancellation_warning && isDesktop() ? (
+
+
+ {edit_icon}
+
+
+ ) : (
+
+ {edit_icon}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+ToggleCardDialog.displayName = 'ToggleCardDialog';
+
+export default React.memo(ToggleCardDialog);
diff --git a/src/components/shared_ui/contract-card/contract-card-items/turbos-card-body.tsx b/src/components/shared_ui/contract-card/contract-card-items/turbos-card-body.tsx
new file mode 100644
index 00000000..5d062d12
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/turbos-card-body.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { addComma, getLimitOrderAmount, isCryptocurrency, isValidToSell } from '@/components/shared';
+
+import ArrowIndicator from '../../arrow-indicator';
+import MobileWrapper from '../../mobile-wrapper';
+import Money from '../../money';
+import { ResultStatusIcon } from '../result-overlay/result-overlay';
+
+import ContractCardItem from './contract-card-item';
+import { TGeneralContractCardBodyProps } from './contract-update-form';
+import ToggleCardDialog from './toggle-card-dialog';
+
+type TTurbosCardBody = Pick<
+ TGeneralContractCardBodyProps,
+ | 'addToast'
+ | 'contract_info'
+ | 'contract_update'
+ | 'currency'
+ | 'current_focus'
+ | 'error_message_alignment'
+ | 'getCardLabels'
+ | 'getContractById'
+ | 'is_sold'
+ | 'onMouseLeave'
+ | 'removeToast'
+ | 'setCurrentFocus'
+> & {
+ progress_slider_mobile_el: React.ReactNode;
+ totalProfit: number;
+};
+
+const TurbosCardBody = ({
+ contract_info,
+ contract_update,
+ currency,
+ getCardLabels,
+ is_sold,
+ progress_slider_mobile_el,
+ ...toggle_card_dialog_props
+}: TTurbosCardBody) => {
+ const {
+ bid_price,
+ buy_price,
+ profit,
+ barrier,
+ entry_spot_display_value,
+ limit_order = {},
+ sell_price,
+ } = contract_info;
+ const { take_profit } = getLimitOrderAmount(contract_update || limit_order);
+ const is_valid_to_sell = isValidToSell(contract_info);
+ const contract_value = is_sold ? sell_price : bid_price;
+ const { BARRIER, CONTRACT_VALUE, ENTRY_SPOT, TAKE_PROFIT, TOTAL_PROFIT_LOSS, STAKE } = getCardLabels();
+
+ return (
+
+
+
+
+
+
+ 0,
+ 'dc-contract-card--loss': Number(profit) < 0,
+ })}
+ >
+
+
+ {!is_sold && (
+
+ )}
+
+
+ {addComma(entry_spot_display_value)}
+
+
+
+
+ {take_profit ? : - }
+ {is_valid_to_sell && (
+
+ )}
+
+
+
+ {addComma(barrier)}
+
+
+
+ {is_sold ? (
+ 0} />
+ ) : (
+ progress_slider_mobile_el
+ )}
+
+
+
+ 0}
+ >
+
+ {!is_sold && }
+
+
+ );
+};
+
+export default React.memo(TurbosCardBody);
diff --git a/src/components/shared_ui/contract-card/contract-card-items/vanilla-options-card-body.tsx b/src/components/shared_ui/contract-card/contract-card-items/vanilla-options-card-body.tsx
new file mode 100644
index 00000000..6b998a4e
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-items/vanilla-options-card-body.tsx
@@ -0,0 +1,137 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { addComma, getDisplayStatus, isCryptocurrency } from '@/components/shared';
+import { TContractInfo } from '@/components/shared/src/utils/contract/contract-types';
+
+import ArrowIndicator from '../../arrow-indicator';
+import DesktopWrapper from '../../desktop-wrapper';
+import MobileWrapper from '../../mobile-wrapper';
+import Money from '../../money';
+import { TGetCardLables } from '../../types';
+import { ResultStatusIcon } from '../result-overlay/result-overlay';
+
+import ContractCardItem from './contract-card-item';
+
+export type TVanillaOptionsCardBodyProps = {
+ contract_info: TContractInfo;
+ currency: string;
+ getCardLabels: TGetCardLables;
+ is_sold: boolean;
+ progress_slider: React.ReactNode;
+};
+
+const VanillaOptionsCardBody: React.FC = ({
+ contract_info,
+ currency,
+ getCardLabels,
+ is_sold,
+ progress_slider,
+}) => {
+ const { buy_price, bid_price, entry_spot_display_value, barrier, sell_price, profit }: TContractInfo =
+ contract_info;
+ const contract_value = is_sold ? sell_price : bid_price;
+ const { CONTRACT_VALUE, ENTRY_SPOT, STAKE, STRIKE, TOTAL_PROFIT_LOSS } = getCardLabels();
+
+ return (
+
+
+
+
+
+
+
+
+ 0,
+ 'dc-contract-card--loss': Number(profit) < 0,
+ })}
+ >
+
+
+ {!is_sold && (
+
+ )}
+
+
+
+ {entry_spot_display_value && addComma(entry_spot_display_value)}
+
+
+
{barrier && addComma(barrier)}
+
+ 0}
+ >
+
+ {!is_sold && }
+
+
+
+
+
+
+
+
+
+
+ {entry_spot_display_value && addComma(entry_spot_display_value)}
+
+
+
+
+
+ 0,
+ 'dc-contract-card--loss': Number(profit) < 0,
+ })}
+ >
+
+
+ {!is_sold && (
+
+ )}
+
+
+
{barrier && addComma(barrier)}
+
+
+ {is_sold ? (
+
+ ) : (
+ progress_slider
+ )}
+
0}
+ >
+
+ {!is_sold && (
+
+ )}
+
+
+
+
+ );
+};
+
+export default React.memo(VanillaOptionsCardBody);
diff --git a/src/components/shared_ui/contract-card/contract-card-loader/contract-card-loader.tsx b/src/components/shared_ui/contract-card/contract-card-loader/contract-card-loader.tsx
new file mode 100644
index 00000000..6999d8f4
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-loader/contract-card-loader.tsx
@@ -0,0 +1,23 @@
+import ContentLoader from 'react-content-loader';
+
+const ContractCardLoader = ({ speed }: { speed: number }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default ContractCardLoader;
diff --git a/src/components/shared_ui/contract-card/contract-card-loader/index.ts b/src/components/shared_ui/contract-card/contract-card-loader/index.ts
new file mode 100644
index 00000000..7c9a15d7
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card-loader/index.ts
@@ -0,0 +1,3 @@
+import ContractCardLoader from './contract-card-loader';
+
+export default ContractCardLoader;
diff --git a/src/components/shared_ui/contract-card/contract-card.scss b/src/components/shared_ui/contract-card/contract-card.scss
new file mode 100644
index 00000000..a59f0790
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card.scss
@@ -0,0 +1,820 @@
+.dc-contract-card {
+ box-sizing: border-box;
+ border-radius: $BORDER_RADIUS;
+ display: flex;
+ flex-direction: column;
+ text-decoration: none;
+ position: relative;
+ padding: 0.8rem;
+ background-color: var(--general-main-1);
+ color: var(--text-prominent);
+ padding-bottom: 0.8rem;
+
+ &:before {
+ content: '';
+ position: absolute;
+ border-radius: $BORDER_RADIUS;
+ height: 52px;
+ width: 100%;
+ left: 0;
+ top: 0;
+ }
+
+ &--green:before {
+ background-image: linear-gradient(to top, rgb(16 19 32 / 0%), rgb(0 148 117 / 16%));
+ }
+
+ &--red:before {
+ background-image: linear-gradient(to top, rgb(16 19 32 / 0%), rgb(227 28 75 / 16%));
+ }
+
+ &--loss {
+ color: var(--text-loss-danger);
+ }
+
+ &--profit {
+ color: var(--text-profit-success);
+ }
+
+ &__grid {
+ display: grid;
+ grid-template-rows: 1fr auto;
+ grid-gap: 4px;
+ min-height: 4rem;
+
+ &-underlying-trade {
+ grid-template-columns: 1fr 1fr;
+ width: 100%;
+
+ &--trader {
+ grid-template-columns: 1.2fr 1fr;
+
+ &--accumulator {
+ display: flex;
+ grid-gap: 1px;
+ }
+
+ &--sold {
+ padding-top: 1rem;
+ }
+ }
+
+ &--mobile {
+ grid-template-columns: 1fr 1fr 25%;
+ }
+ }
+
+ &-profit-payout {
+ grid-template-columns: 1fr 1fr;
+ padding: 8px 0;
+ border-radius: $BORDER_RADIUS;
+ margin-left: -4px;
+ margin-right: -4px;
+ background: var(--general-hover);
+ }
+
+ &-label {
+ font-size: 1rem;
+ align-self: flex-start;
+ text-transform: none;
+ line-height: 1.5;
+ color: var(--text-general);
+ white-space: nowrap;
+ }
+
+ &-value {
+ font-size: 1.2rem;
+ font-weight: bold;
+ align-self: flex-start;
+ line-height: 1.5;
+ }
+
+ &-items {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 0.8rem 0.4rem;
+ padding: 0.8rem 0;
+ }
+ }
+
+ &__result {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: calc(100% - 1rem);
+ border-radius: $BORDER_RADIUS;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--overlay-inside-dialog);
+
+ &--won,
+ &--won:after {
+ background-image: var(--gradient-success);
+ }
+
+ &--lost,
+ &--lost:after {
+ background-image: var(--gradient-danger);
+ }
+
+ &--lg {
+ max-height: 100%;
+ }
+
+ &:hover {
+ background: 0 !important;
+
+ .dc-result__content {
+ opacity: 0;
+ }
+ }
+
+ &:after {
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: $BORDER_RADIUS;
+ z-index: 2;
+ }
+
+ &--enter,
+ &--exit {
+ opacity: 0;
+ }
+
+ &--enter-done {
+ opacity: 1;
+ }
+ }
+
+ &__wrapper {
+ margin: 0 1em;
+ min-width: 218px;
+ max-width: 238px;
+ width: min-content;
+ border-radius: $BORDER_RADIUS;
+ transition:
+ transform 0.25s ease,
+ opacity 0.25s linear;
+ position: relative;
+ background: var(--general-main-1);
+
+ .currency-badge {
+ margin-bottom: 5px;
+ }
+
+ &--active:before {
+ content: '';
+ top: -1px;
+ left: -1px;
+ width: calc(100% + 2px);
+ height: calc(100% + 2px);
+ position: absolute;
+ box-shadow: 0 4px 6px 0 rgb(0 0 0 / 24%);
+ border-radius: $BORDER_RADIUS;
+ pointer-events: none;
+ }
+
+ &--enter-done {
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ &--enter,
+ &--exit {
+ opacity: 0;
+ transform: translateY(-16px);
+ }
+ }
+
+ .purchase-price,
+ .potential-payout {
+ &-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ }
+
+ &__label {
+ font-size: 1rem;
+ align-self: flex-start;
+ text-transform: none;
+ margin-bottom: 4px;
+ color: var(--text-general);
+ white-space: nowrap;
+ }
+
+ &__value {
+ font-size: 1em;
+ font-weight: bold;
+ align-self: flex-start;
+ }
+ }
+
+ .potential-payout {
+ &-container {
+ padding-left: 4px;
+ }
+
+ &-price__value {
+ font-size: 1em;
+ font-weight: bold;
+ align-self: flex-start;
+ }
+ }
+
+ &__separator {
+ width: 100%;
+ border-top: 1px solid var(--general-section-1);
+ margin: 4px 0 0;
+ }
+
+ &__underlying-name {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ font-weight: bold;
+ font-size: 1.2em;
+ max-width: 18rem;
+ min-width: 10rem;
+
+ &--accumulator {
+ max-width: 10rem;
+ }
+ }
+
+ &__type {
+ font-size: 1.2em;
+ display: flex;
+ justify-content: flex-start;
+ font-weight: bold;
+
+ /* postcss-bem-linter: ignore */
+ .category-type {
+ .color1-fill {
+ fill: var(--brand-red-coral) !important;
+ }
+
+ .color2-fill {
+ fill: var(--brand-secondary) !important;
+ }
+ }
+ }
+
+ &__symbol {
+ /* rtl:ignore */
+ margin-left: 0.4rem;
+
+ &--smarttrader-contract {
+ width: 7rem;
+
+ @include mobile {
+ width: initial;
+ }
+ }
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-evenly;
+ flex-direction: column;
+ margin: 0.5em 0.2em;
+
+ @include mobile {
+ margin: 0 8px;
+ }
+ }
+
+ &__body {
+ border-radius: $BORDER_RADIUS;
+ width: 100%;
+ padding: 0.8rem 0.4rem;
+ color: var(--text-prominent);
+ display: flex;
+
+ &-wrapper {
+ width: 100%;
+ }
+
+ @include mobile {
+ padding: 0 8px;
+
+ &-wrapper {
+ display: flex;
+ }
+ }
+ }
+
+ &-items-wrapper {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas:
+ 'stake current-stake'
+ 'deal-cancel-fee limit-order-info'
+ 'buy-price limit-order-info';
+ grid-gap: 0.8rem 0.4rem;
+ flex: 1;
+ margin-top: 0.5rem;
+
+ &--mobile {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-template-areas:
+ 'stake current-stake limit-order-info'
+ 'deal-cancel-fee buy-price limit-order-info';
+ grid-gap: 0.8rem 0.4rem;
+ flex: 1;
+ padding: 0.4rem 0;
+
+ .dc-contract-card-items-wrapper-group {
+ .dc-contract-card-item:first-child {
+ margin-bottom: 0.8rem;
+ }
+ }
+ }
+
+ &--has-progress-slider {
+ grid-template-columns: 1fr 1fr 1fr;
+ grid-template-areas:
+ 'stake current-stake date-expiry'
+ 'deal-cancel-fee buy-price date-expiry'
+ 'limit-order-info limit-order-info date-expiry';
+
+ .dc-contract-card {
+ &__limit-order-info {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ &__stop-loss {
+ order: unset !important;
+ }
+ }
+ }
+ }
+
+ &--turbos,
+ &--lookbacks {
+ .dc-contract-card {
+ &-item {
+ &__body {
+ @include mobile {
+ padding-left: 0;
+ }
+ }
+ }
+
+ &__buy-price {
+ grid-column: 1/1;
+ grid-row: 1/1;
+ padding: 0.8rem 0 0;
+ }
+
+ &__contract-value {
+ grid-column: 2/2;
+ grid-row: 1/2;
+ padding: 0.8rem 0 0;
+ }
+
+ &__entry-spot {
+ grid-column: 1/2;
+ grid-row: 2/2;
+ }
+
+ &__barrier-level {
+ grid-column: 1/1;
+ grid-row: 3/3;
+ }
+
+ &__limit-order-info {
+ grid-row: 2/2;
+ grid-column: 2/2;
+ }
+
+ &__status {
+ place-self: center center;
+ grid-row: 1/3;
+ grid-column: 3/3;
+ }
+ }
+
+ @include mobile {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+ }
+
+ &__profit-loss {
+ font-size: 1.2em;
+ text-align: center;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ font-weight: bold;
+
+ &-label {
+ font-size: 1em;
+ margin-bottom: 4px;
+ font-weight: normal;
+ white-space: nowrap;
+ max-width: 90px;
+ }
+
+ &--is-crypto {
+ margin-left: -6px;
+ }
+
+ &--negative {
+ color: var(--text-loss-danger);
+
+ &:before {
+ content: '-';
+ color: inherit;
+ }
+ }
+
+ &--positive {
+ color: var(--text-profit-success);
+
+ &:before {
+ content: '+';
+ color: inherit;
+ }
+ }
+ }
+
+ &-item {
+ &__header {
+ max-width: calc(100% - 2rem);
+ line-height: 1.4;
+
+ @include mobile {
+ max-width: unset;
+ }
+ }
+
+ &__body {
+ @include typeface(--small-left-bold-black);
+
+ display: flex;
+ align-self: flex-start;
+ line-height: 1.5;
+
+ &--is-crypto {
+ margin-left: -6px;
+ }
+
+ @include mobile {
+ padding-left: 0.4rem;
+ }
+ }
+
+ &__body--loss {
+ color: var(--text-loss-danger) !important;
+
+ &:before {
+ content: '-';
+ }
+ }
+
+ &__body--profit {
+ color: var(--text-profit-success) !important;
+
+ &:before {
+ content: '+';
+ }
+ }
+
+ &__total-profit-loss {
+ padding: 0.8rem 0.8rem 0;
+ border-top: 1px solid var(--general-section-1);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: 0.8rem;
+
+ .dc-contract-card-item__header,
+ .dc-contract-card-item__body {
+ font-size: 1.4rem;
+ line-height: 2rem;
+ align-self: center;
+ }
+
+ @include mobile {
+ flex-direction: row;
+ justify-content: center;
+ grid-column: 1 / 4;
+
+ &-value {
+ margin-left: 0.2rem;
+ }
+ }
+
+ &-label,
+ &-value {
+ font-size: 1.4rem;
+ line-height: 2rem;
+ }
+ }
+
+ &__payout-limit {
+ padding: 0.8rem 0 0;
+ border-top: 1px solid var(--general-section-1);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .dc-contract-card-item__body {
+ align-self: center;
+ }
+ }
+
+ &__footer {
+ .lookbacks--note {
+ margin-top: 0.4rem;
+ }
+ }
+ }
+
+ &__sell-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition:
+ transform 0.25s ease,
+ opacity 0.25s linear;
+ padding-top: 0.4rem;
+ margin-top: 0.8rem;
+ border-top: 1px solid var(--general-section-1);
+ min-height: 4rem;
+
+ &--positions {
+ padding: 0.8rem 0;
+ margin: 0 0.8rem;
+ }
+
+ &--enter-done {
+ opacity: 1;
+ }
+
+ &--enter,
+ &--exit {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+
+ &--has-cancel-btn {
+ justify-content: space-between;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ .dc-btn--cancel {
+ /* rtl:ignore */
+ margin-left: 0.8rem;
+ max-width: 10.5rem;
+ }
+ }
+
+ .dc-remaining-time {
+ border-radius: 0.4rem;
+ font-size: 1.2rem;
+ margin-left: 0.8rem;
+ }
+
+ .dc-btn--sell,
+ .dc-btn--cancel {
+ height: 2.4rem;
+
+ .dc-btn__text {
+ font-size: 1.2rem;
+ }
+
+ @include mobile {
+ height: 3.2rem;
+ }
+ }
+ }
+
+ &__indicative--movement {
+ margin-left: 2px;
+ width: 16px;
+ height: 16px;
+
+ &:after {
+ content: '';
+ width: 16px;
+ }
+ }
+
+ &__status {
+ min-width: 25%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .result-icon,
+ &--accumulator-mobile-positions .dc-result__icon {
+ margin-left: 0.4rem;
+ }
+ }
+
+ &__limit-order-info {
+ grid-area: limit-order-info;
+ display: grid;
+ grid-gap: 0.4rem 0;
+ position: relative;
+
+ & .dc-contract-card__stop-loss {
+ padding-bottom: 0.4rem;
+ }
+ }
+
+ &__take-profit {
+ position: relative;
+ }
+
+ &__buy-price {
+ grid-area: buy-price;
+ }
+
+ &__deal-cancel-fee {
+ grid-area: deal-cancel-fee;
+
+ &__disabled {
+ color: var(--text-disabled-1);
+
+ .dc-contract-card-item__body {
+ color: inherit;
+ }
+ }
+ }
+
+ &__stake {
+ grid-area: stake;
+ }
+
+ &__date-expiry {
+ grid-area: date-expiry;
+
+ .dc-contract-card-item__body {
+ justify-content: flex-end;
+ }
+ }
+
+ &__current-stake {
+ grid-area: current-stake;
+ }
+
+ &__sell-button-mobile {
+ place-self: center center;
+
+ .dc-btn--sell {
+ height: 3.2rem !important;
+ }
+ }
+
+ &__no-resale-msg {
+ font-size: 1.1rem;
+ }
+
+ .contract-audit {
+ &__wrapper {
+ padding: 2px 0;
+ width: 100%;
+ }
+
+ &__wrapper--is-open {
+ margin-top: 0.5em;
+ }
+
+ &__toggle {
+ margin: 0 0 -0.5em;
+ width: 100%;
+ }
+
+ &__label {
+ font-size: 0.8em;
+ text-align: left;
+ }
+
+ &__value {
+ font-size: 1em;
+ text-align: left;
+ }
+ }
+
+ .dc-contract-type__type-wrapper {
+ width: unset;
+ height: unset;
+ }
+}
+
+/** @define dc-contract-type */
+.dc-contract-type {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ text-align: center;
+ margin-left: -4px;
+
+ &__type-wrapper {
+ margin: 0;
+ padding: 0.5em 0.3em;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ /* postcss-bem-linter: ignore */
+ .category-type {
+ width: 20px;
+ height: 20px;
+ }
+ }
+
+ &__type-label {
+ width: 3.8em;
+ line-height: 1.5;
+
+ /* rtl:ignore */
+ text-align: left;
+
+ &--smarttrader-contract,
+ &--multipliers {
+ width: 7rem;
+
+ @include mobile {
+ width: 9rem;
+ }
+ }
+
+ &--lookbacks-contract {
+ width: 5rem;
+
+ @include mobile {
+ width: initial;
+ }
+ }
+
+ &-trade-param {
+ font-size: 1rem;
+ line-height: 1rem;
+ color: var(--text-less-prominent);
+ }
+ }
+}
+
+/** @define dc-btn; */
+.dc-btn {
+ &--sell {
+ width: 100%;
+ }
+}
+
+/** @define dc-remaining-time; weak */
+.dc-remaining-time {
+ display: inline;
+}
+
+/** @define dc-tick-counter-bar; */
+.dc-tick-counter-bar {
+ &__container {
+ grid-column: 1 / 3;
+ position: relative;
+ width: 100%;
+ padding: unset;
+ box-sizing: border-box;
+ margin: 0.8rem 0;
+ border-bottom: 1px solid var(--general-section-1);
+
+ @include mobile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-bottom: unset;
+ margin: unset;
+ }
+ }
+
+ &__track {
+ height: 1.8rem;
+ background: var(--general-section-1);
+ border-radius: #{$BORDER_RADIUS};
+ position: relative;
+ margin-bottom: 0.8rem;
+ width: 100%;
+
+ @include mobile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ border-bottom: unset;
+ margin: 0.2rem 0 0.4rem;
+ }
+ }
+
+ &__text {
+ position: absolute;
+ width: 100%;
+ }
+}
diff --git a/src/components/shared_ui/contract-card/contract-card.tsx b/src/components/shared_ui/contract-card/contract-card.tsx
new file mode 100644
index 00000000..98142676
--- /dev/null
+++ b/src/components/shared_ui/contract-card/contract-card.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+
+import { getTotalProfit } from '@/components/shared/utils/contract';
+
+import DesktopWrapper from '../desktop-wrapper';
+import { TGetCardLables, TGetContractPath } from '../types';
+
+import ContractCardBody from './contract-card-items/contract-card-body';
+import ContractCardFooter from './contract-card-items/contract-card-footer';
+import ContractCardHeader from './contract-card-items/contract-card-header';
+import ContractCardSell from './contract-card-items/contract-card-sell';
+import ContractTypeCell from './contract-card-items/contract-type-cell';
+import MultiplierCloseActions from './contract-card-items/multiplier-close-actions';
+import ContractCardLoader from './contract-card-loader';
+import ResultOverlay from './result-overlay';
+
+type TContractCardProps = {
+ contract_info: TContractInfo;
+ getCardLabels: TGetCardLables;
+ getContractPath?: TGetContractPath;
+ is_multiplier: boolean;
+ is_positions?: boolean;
+ onClickRemove?: (contract_id?: number) => void;
+ profit_loss: number;
+ result?: string;
+ should_show_result_overlay: boolean;
+};
+
+const ContractCard = ({
+ children,
+ contract_info,
+ getCardLabels,
+ getContractPath,
+ is_multiplier,
+ is_positions,
+ onClickRemove,
+ profit_loss,
+ result,
+ should_show_result_overlay,
+}: React.PropsWithChildren) => {
+ const fallback_result = profit_loss >= 0 ? 'won' : 'lost';
+ const payout_info = is_multiplier ? getTotalProfit(contract_info) : profit_loss;
+ return (
+
+ {should_show_result_overlay && (
+
+
+
+ )}
+ {children}
+
+ );
+};
+
+ContractCard.Header = ContractCardHeader;
+ContractCard.Body = ContractCardBody;
+ContractCard.Footer = ContractCardFooter;
+ContractCard.Loader = ContractCardLoader;
+ContractCard.ContractTypeCell = ContractTypeCell;
+ContractCard.MultiplierCloseActions = MultiplierCloseActions;
+ContractCard.Sell = ContractCardSell;
+
+export default ContractCard;
diff --git a/src/components/shared_ui/contract-card/index.ts b/src/components/shared_ui/contract-card/index.ts
new file mode 100644
index 00000000..64d258fd
--- /dev/null
+++ b/src/components/shared_ui/contract-card/index.ts
@@ -0,0 +1,5 @@
+import ContractCard from './contract-card';
+
+import './contract-card.scss';
+
+export default ContractCard;
diff --git a/src/components/shared_ui/contract-card/result-overlay/index.ts b/src/components/shared_ui/contract-card/result-overlay/index.ts
new file mode 100644
index 00000000..6c570cf1
--- /dev/null
+++ b/src/components/shared_ui/contract-card/result-overlay/index.ts
@@ -0,0 +1,5 @@
+import ResultOverlay from './result-overlay';
+
+import './result-overlay.scss';
+
+export default ResultOverlay;
diff --git a/src/components/shared_ui/contract-card/result-overlay/result-overlay.scss b/src/components/shared_ui/contract-card/result-overlay/result-overlay.scss
new file mode 100644
index 00000000..db1bb372
--- /dev/null
+++ b/src/components/shared_ui/contract-card/result-overlay/result-overlay.scss
@@ -0,0 +1,60 @@
+/** @define dc-result */
+.dc-result {
+ &__content {
+ display: flex;
+ justify-items: center;
+ align-items: center;
+ flex-direction: column;
+ gap: 0.4rem;
+ }
+ &__caption {
+ text-transform: capitalize;
+ font-size: 1.4em;
+ font-weight: bold;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ transition: opacity 0.25s ease;
+ gap: 0.4rem;
+
+ &--won {
+ color: var(--text-profit-success);
+ }
+ &--lost {
+ color: var(--text-loss-danger);
+ }
+ &-wrapper {
+ cursor: pointer;
+ width: 100%;
+ height: calc(100% - 24px);
+ text-decoration: none;
+ position: absolute;
+ top: 24px;
+ z-index: 2;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+ @include mobile {
+ pointer-events: none;
+ }
+ }
+ &__close-btn {
+ position: absolute;
+ top: 2px;
+ /*rtl:ignore*/
+ right: 8px;
+ cursor: pointer;
+ visibility: visible;
+
+ &:after {
+ content: '\0000D7';
+ font-size: 24px;
+ font-weight: 300;
+ color: var(--text-general);
+ }
+ }
+ &__positions-overlay {
+ max-width: 218px;
+ }
+}
diff --git a/src/components/shared_ui/contract-card/result-overlay/result-overlay.tsx b/src/components/shared_ui/contract-card/result-overlay/result-overlay.tsx
new file mode 100644
index 00000000..929b20ae
--- /dev/null
+++ b/src/components/shared_ui/contract-card/result-overlay/result-overlay.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import Money from '../../money';
+import Text from '../../text';
+import { TGetCardLables, TGetContractPath } from '../../types';
+
+type TResultOverlayProps = {
+ currency?: string;
+ contract_id?: number;
+ getCardLabels: TGetCardLables;
+ getContractPath?: TGetContractPath;
+ is_multiplier?: boolean;
+ is_positions?: boolean;
+ is_visible: boolean;
+ onClickRemove?: (contract_id?: number) => void;
+ payout_info: number;
+ result: string;
+};
+
+type TResultStatusIcon = {
+ getCardLabels: TGetCardLables;
+ is_contract_won?: boolean;
+};
+
+export const ResultStatusIcon = ({ getCardLabels, is_contract_won }: TResultStatusIcon) => (
+
+
+ {getCardLabels().CLOSED}
+
+);
+
+const ResultOverlay = ({
+ currency,
+ contract_id,
+ getCardLabels,
+ getContractPath,
+ is_positions,
+ is_visible,
+ onClickRemove,
+ payout_info,
+ result,
+}: TResultOverlayProps) => {
+ const is_contract_won = result === 'won';
+
+ return (
+
+
+
+ {is_positions && (
+
{
+ if (contract_id) onClickRemove?.(contract_id);
+ }}
+ />
+ )}
+ {getContractPath && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ResultOverlay;
diff --git a/src/components/shared_ui/counter/counter.scss b/src/components/shared_ui/counter/counter.scss
new file mode 100644
index 00000000..b10053a5
--- /dev/null
+++ b/src/components/shared_ui/counter/counter.scss
@@ -0,0 +1,12 @@
+.dc-counter {
+ align-items: center;
+ background: var(--status-danger);
+ border-radius: 0.8rem;
+ color: var(--text-colored-background);
+ display: inline-flex;
+ font-size: 1rem;
+ font-weight: bold;
+ justify-content: center;
+ line-height: 1;
+ padding: 3px 5px;
+}
diff --git a/src/components/shared_ui/counter/counter.tsx b/src/components/shared_ui/counter/counter.tsx
new file mode 100644
index 00000000..ddf69b27
--- /dev/null
+++ b/src/components/shared_ui/counter/counter.tsx
@@ -0,0 +1,11 @@
+import classNames from 'classnames';
+
+type TCounterProps = {
+ className?: string;
+ count: number;
+};
+const Counter = ({ className, count }: TCounterProps) => {
+ return {count}
;
+};
+
+export default Counter;
diff --git a/src/components/shared_ui/counter/index.ts b/src/components/shared_ui/counter/index.ts
new file mode 100644
index 00000000..f27b92b0
--- /dev/null
+++ b/src/components/shared_ui/counter/index.ts
@@ -0,0 +1,5 @@
+import Counter from './counter';
+
+import './counter.scss';
+
+export default Counter;
diff --git a/src/components/shared_ui/currency-badge/currency-badge.scss b/src/components/shared_ui/currency-badge/currency-badge.scss
new file mode 100644
index 00000000..ad40ade5
--- /dev/null
+++ b/src/components/shared_ui/currency-badge/currency-badge.scss
@@ -0,0 +1,7 @@
+/** @define .dc-currency-badge */
+.dc-currency-badge {
+ align-self: flex-start;
+ padding: 0.3rem;
+ background-color: $color-green;
+ border-radius: 4px;
+}
diff --git a/src/components/shared_ui/currency-badge/currency-badge.tsx b/src/components/shared_ui/currency-badge/currency-badge.tsx
new file mode 100644
index 00000000..732a33a4
--- /dev/null
+++ b/src/components/shared_ui/currency-badge/currency-badge.tsx
@@ -0,0 +1,15 @@
+import { getCurrencyDisplayCode } from '@/components/shared';
+
+import Text from '../text';
+
+type TCurrencyBadgeProps = {
+ currency: string;
+};
+
+const CurrencyBadge = ({ currency }: TCurrencyBadgeProps) => (
+
+ {getCurrencyDisplayCode(currency)}
+
+);
+
+export default CurrencyBadge;
diff --git a/src/components/shared_ui/currency-badge/index.ts b/src/components/shared_ui/currency-badge/index.ts
new file mode 100644
index 00000000..731ed75a
--- /dev/null
+++ b/src/components/shared_ui/currency-badge/index.ts
@@ -0,0 +1,5 @@
+import CurrencyBadge from './currency-badge';
+
+import './currency-badge.scss';
+
+export default CurrencyBadge;
diff --git a/src/components/shared_ui/data-list/data-list-cell.tsx b/src/components/shared_ui/data-list/data-list-cell.tsx
new file mode 100644
index 00000000..26233c0d
--- /dev/null
+++ b/src/components/shared_ui/data-list/data-list-cell.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { isTurbosContract, isVanillaContract } from '@/components/shared';
+
+import { TPassThrough, TRow } from '../types/common.types';
+
+export type TColIndex =
+ | 'type'
+ | 'reference'
+ | 'currency'
+ | 'purchase'
+ | 'payout'
+ | 'profit'
+ | 'indicative'
+ | 'id'
+ | 'multiplier'
+ | 'buy_price'
+ | 'cancellation'
+ | 'limit_order'
+ | 'bid_price'
+ | 'action';
+
+export type TRenderCellContent = {
+ cell_value: string;
+ is_footer?: boolean;
+ passthrough?: TPassThrough;
+ row_obj: TRow;
+ is_turbos?: boolean;
+ is_vanilla?: boolean;
+};
+export type THeaderProps = {
+ title?: React.ReactNode;
+ is_vanilla?: boolean;
+};
+
+export type TDataListCell = {
+ className?: string;
+ column?: {
+ key?: string;
+ title?: string;
+ col_index?: TColIndex;
+ renderCellContent?: (props: TRenderCellContent) => React.ReactNode;
+ renderHeader?: (prop: renderHeaderType) => React.ReactNode;
+ };
+ is_footer?: boolean;
+ passthrough?: TPassThrough;
+ row?: TRow;
+};
+
+type renderHeaderType = { title?: string; is_vanilla?: boolean };
+
+const DataListCell = ({ className, column, is_footer, passthrough, row }: TDataListCell) => {
+ if (!column) return null;
+ const { col_index, title } = column;
+ const cell_value = row?.[col_index as TColIndex];
+ const is_turbos = isTurbosContract(row?.contract_info?.contract_type);
+ const is_vanilla = isVanillaContract(row?.contract_info?.contract_type);
+
+ return (
+
+ {!is_footer && (
+
+ {column.renderHeader ? column.renderHeader({ title, is_vanilla }) : title}
+
+ )}
+
+ {column.renderCellContent
+ ? column.renderCellContent({
+ cell_value,
+ is_footer,
+ passthrough,
+ row_obj: row as TRow,
+ is_vanilla,
+ is_turbos,
+ })
+ : cell_value}
+
+
+ );
+};
+
+export default React.memo(DataListCell);
diff --git a/src/components/shared_ui/data-list/data-list-row.tsx b/src/components/shared_ui/data-list/data-list-row.tsx
new file mode 100644
index 00000000..d22113fe
--- /dev/null
+++ b/src/components/shared_ui/data-list/data-list-row.tsx
@@ -0,0 +1,117 @@
+import React from 'react';
+import { NavLink } from 'react-router-dom';
+import classNames from 'classnames';
+
+import { clickAndKeyEventHandler, useIsMounted } from '@/components/shared';
+import { useDebounce } from '@/hooks/use-debounce';
+
+import { TSource } from '../data-table/table-row';
+import { TPassThrough, TRow } from '../types/common.types';
+
+import { TColIndex, TDataListCell } from './data-list-cell';
+
+type TMobileRowRenderer = {
+ row?: TRow;
+ is_footer?: boolean;
+ columns_map?: Record;
+ server_time?: moment.Moment;
+ onClickCancel: (contract_id?: number) => void;
+ onClickSell: (contract_id?: number) => void;
+ measure?: () => void;
+ passthrough?: TPassThrough;
+};
+
+export type TRowRenderer = (params: Partial) => React.ReactNode;
+
+type TDataListRow = {
+ action_desc?: {
+ component: React.ReactNode;
+ };
+ destination_link?: string;
+ row_gap?: number;
+ row_key: string | number;
+ rowRenderer: TRowRenderer;
+ measure?: () => void;
+ is_dynamic_height?: boolean;
+ is_new_row: boolean;
+ is_scrolling: boolean;
+ passthrough?: TPassThrough;
+ row: TSource;
+};
+
+const DataListRow = ({
+ action_desc,
+ destination_link,
+ row_gap,
+ row_key,
+ rowRenderer,
+ measure,
+ is_dynamic_height,
+ ...other_props
+}: TDataListRow) => {
+ const [show_description, setShowDescription] = React.useState(false);
+ const isMounted = useIsMounted();
+ const debouncedHideDetails = useDebounce(() => setShowDescription(false), 5000);
+
+ const toggleDetails = () => {
+ if (action_desc) {
+ setShowDescription(!show_description);
+ debouncedHideDetails();
+ }
+ };
+
+ const toggleDetailsDecorator = (e?: React.MouseEvent | React.KeyboardEvent) => {
+ clickAndKeyEventHandler(toggleDetails, e);
+ };
+
+ React.useEffect(() => {
+ if (isMounted() && is_dynamic_height) {
+ measure?.();
+ }
+ }, [show_description, is_dynamic_height, measure]);
+
+ return (
+
+ {destination_link ? (
+
+ {rowRenderer({ measure, ...other_props })}
+
+ ) : (
+
+ {action_desc ? (
+
+ {show_description ? (
+
+ {action_desc.component &&
{action_desc.component}
}
+
+ ) : (
+ rowRenderer({ measure, ...other_props })
+ )}
+
+ ) : (
+
{rowRenderer({ measure, ...other_props })}
+ )}
+
+ )}
+
+ );
+};
+
+export default React.memo(DataListRow);
diff --git a/src/components/shared_ui/data-list/data-list.scss b/src/components/shared_ui/data-list/data-list.scss
new file mode 100644
index 00000000..7de0e18b
--- /dev/null
+++ b/src/components/shared_ui/data-list/data-list.scss
@@ -0,0 +1,102 @@
+/** @define data-list; weak */
+.data-list {
+ position: relative;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+
+ &__body {
+ flex: 1;
+
+ &-wrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ }
+ &__footer {
+ width: 100%;
+ background: var(--general-main-1);
+ border-top: 2px solid var(--border-disabled);
+ display: flex;
+ align-items: center;
+ position: relative;
+ }
+ &__item {
+ height: inherit;
+
+ &--wrapper {
+ height: inherit;
+ text-decoration: none;
+ -webkit-touch-callout: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+ @include mobile {
+ border-radius: $BORDER_RADIUS;
+ }
+ }
+ &__row {
+ display: flex;
+ flex-direction: row;
+ padding: 4px 16px;
+ width: 100%;
+
+ > * {
+ flex: 1;
+ }
+ &-content {
+ font-size: 1.4rem;
+ line-height: 2rem;
+ color: var(--text-general);
+ }
+ &-cell {
+ &--amount {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ flex: none;
+ }
+ }
+ &-title {
+ font-size: 1.4rem;
+ font-weight: bold;
+ color: var(--text-prominent);
+ line-height: 2rem;
+
+ @include mobile {
+ font-size: 1.2rem;
+ }
+ }
+ &-divider {
+ margin: 0 1.6rem;
+
+ &:after {
+ content: '';
+ display: block;
+ border-top: 1px solid var(--general-main-1);
+ }
+ }
+ &--wrapper:not(.data-list__item--dynamic-height-wrapper) {
+ height: 100%;
+ }
+ &--timer {
+ flex: none;
+ }
+ }
+ &__desc {
+ &--wrapper {
+ height: inherit;
+ display: flex;
+ text-align: center;
+ align-items: center;
+ font-size: var(--text-size-xxs);
+ color: var(--text-general);
+ padding: 1rem;
+ }
+ }
+}
+
+/* stylelint-disable-next-line plugin/selector-bem-pattern */
+.ReactVirtualized__List {
+ outline: 0;
+}
diff --git a/src/components/shared_ui/data-list/data-list.tsx b/src/components/shared_ui/data-list/data-list.tsx
new file mode 100644
index 00000000..701a2f13
--- /dev/null
+++ b/src/components/shared_ui/data-list/data-list.tsx
@@ -0,0 +1,239 @@
+import React from 'react';
+import { TransitionGroup } from 'react-transition-group';
+import {
+ AutoSizer as _AutoSizer,
+ type AutoSizerProps,
+ CellMeasurer as _CellMeasurer,
+ CellMeasurerCache,
+ CellMeasurerProps,
+ IndexRange,
+ List as _List,
+ ListProps,
+ ListRowProps,
+} from 'react-virtualized';
+import { MeasuredCellParent } from 'react-virtualized/dist/es/CellMeasurer';
+import classNames from 'classnames';
+
+import { isDesktop, isForwardStartingBuyTransaction, isMobile } from '@/components/shared';
+
+import ThemedScrollbars from '../themed-scrollbars';
+import { TPassThrough, TRow, TTableRowItem } from '../types/common.types';
+
+import DataListCell from './data-list-cell';
+import DataListRow, { TRowRenderer } from './data-list-row';
+
+const List = _List as unknown as React.FC;
+const AutoSizer = _AutoSizer as unknown as React.FC;
+const CellMeasurer = _CellMeasurer as unknown as React.FC;
+
+export type TDataList = {
+ className?: string;
+ data_source: TRow[];
+ footer?: TRow;
+ getRowAction?: (row: TRow) => TTableRowItem;
+ getRowSize?: (params: { index: number }) => number;
+ keyMapper?: (row: TRow) => number | string;
+ onRowsRendered?: (params: IndexRange) => void;
+ onScroll?: React.UIEventHandler;
+ passthrough?: TPassThrough;
+ row_gap?: number;
+ setListRef?: (ref: MeasuredCellParent) => void;
+ rowRenderer: TRowRenderer;
+ children?: React.ReactNode;
+ overscanRowCount?: number;
+};
+type GetContentType = { measure?: () => void | undefined };
+
+const DataList = React.memo(
+ ({
+ children,
+ className,
+ data_source,
+ footer,
+ getRowSize,
+ keyMapper,
+ onRowsRendered,
+ onScroll,
+ setListRef,
+ overscanRowCount,
+ ...other_props
+ }: TDataList) => {
+ const [is_loading, setLoading] = React.useState(true);
+ const [is_scrolling, setIsScrolling] = React.useState(false);
+ const [scroll_top, setScrollTop] = React.useState(0);
+
+ const cache = React.useRef();
+ const list_ref = React.useRef(null);
+ const items_transition_map_ref = React.useRef<{ [key: string]: boolean }>({});
+ const data_source_ref = React.useRef(null);
+ data_source_ref.current = data_source;
+
+ const is_dynamic_height = !getRowSize;
+
+ const trackItemsForTransition = React.useCallback(() => {
+ data_source.forEach((item: TRow, index: number) => {
+ const row_key: string | number = keyMapper?.(item) || `${index}-0`;
+ items_transition_map_ref.current[row_key] = true;
+ });
+ }, [data_source, keyMapper]);
+
+ React.useEffect(() => {
+ if (is_dynamic_height) {
+ cache.current = new CellMeasurerCache({
+ fixedWidth: true,
+ keyMapper: row_index => {
+ if (data_source_ref?.current && row_index < data_source_ref?.current.length)
+ return keyMapper?.(data_source_ref.current[row_index]) || row_index;
+ return row_index;
+ },
+ });
+ }
+ trackItemsForTransition();
+ setLoading(false);
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ React.useEffect(() => {
+ if (is_dynamic_height) {
+ list_ref.current?.recomputeGridSize?.({ columnIndex: 0, rowIndex: 0 });
+ }
+ trackItemsForTransition();
+ }, [data_source, is_dynamic_height, trackItemsForTransition]);
+
+ const footerRowRenderer = () => {
+ return {other_props.rowRenderer({ row: footer, is_footer: true })} ;
+ };
+
+ const rowRenderer = ({ style, index, key, parent }: ListRowProps) => {
+ const { getRowAction, passthrough, row_gap } = other_props;
+ let row = data_source[index];
+ const { action_type, shortcode, purchase_time, transaction_time, id } = row;
+ if (isForwardStartingBuyTransaction(action_type, shortcode, purchase_time || transaction_time)) {
+ const is_sold = !!data_source?.find(
+ transaction => transaction.action_type === 'sell' && transaction.id === id
+ );
+ row = { ...row, is_sold };
+ }
+ const action = getRowAction && getRowAction(row);
+ const destination_link = typeof action === 'string' ? action : undefined;
+ const action_desc = typeof action === 'object' ? action : undefined;
+ const row_key = keyMapper?.(row) || key;
+
+ const getContent = ({ measure }: GetContentType = {}) => (
+
+ );
+
+ return is_dynamic_height && cache.current ? (
+
+ {({ measure }) => {getContent({ measure })}
}
+
+ ) : (
+
+ {getContent()}
+
+ );
+ };
+
+ const handleScroll = (ev: Partial>) => {
+ let timeout;
+
+ clearTimeout(timeout);
+ if (!is_scrolling) {
+ setIsScrolling(true);
+ }
+ timeout = setTimeout(() => {
+ if (!is_loading) {
+ setIsScrolling(false);
+ }
+ }, 200);
+
+ setScrollTop((ev.target as HTMLElement).scrollTop);
+ if (typeof onScroll === 'function') {
+ onScroll(ev as React.UIEvent);
+ }
+ };
+
+ const setRef = (ref: MeasuredCellParent) => {
+ list_ref.current = ref;
+ setListRef?.(ref);
+ };
+
+ if (is_loading) {
+ return
;
+ }
+ return (
+
+
+
+
+ {({ width, height }) => (
+ // Don't remove `TransitionGroup`. When `TransitionGroup` is removed, transition life cycle events like `onEntered` won't be fired sometimes on it's `CSSTransition` children
+
+
+ setRef(ref)}
+ rowCount={data_source.length}
+ rowHeight={
+ is_dynamic_height && cache?.current?.rowHeight
+ ? cache?.current?.rowHeight
+ : getRowSize || 0
+ }
+ rowRenderer={rowRenderer}
+ scrollingResetTimeInterval={0}
+ width={width}
+ {...(isDesktop()
+ ? { scrollTop: scroll_top, autoHeight: true }
+ : {
+ onScroll: target =>
+ handleScroll({ target } as unknown as Partial<
+ React.UIEvent
+ >),
+ })}
+ />
+
+
+ )}
+
+
+ {children}
+
+ {footer && (
+
+ {footerRowRenderer()}
+
+ )}
+
+ );
+ }
+) as React.MemoExoticComponent<(props: TDataList) => JSX.Element> & { Cell: typeof DataListCell };
+
+DataList.displayName = 'DataList';
+DataList.Cell = DataListCell;
+
+export default DataList;
diff --git a/src/components/shared_ui/data-list/index.ts b/src/components/shared_ui/data-list/index.ts
new file mode 100644
index 00000000..562922c7
--- /dev/null
+++ b/src/components/shared_ui/data-list/index.ts
@@ -0,0 +1,5 @@
+import DataList from './data-list';
+
+import './data-list.scss';
+
+export default DataList;
diff --git a/src/components/index.ts b/src/components/shared_ui/desktop-wrapper/desktop-wrapper.scss
similarity index 100%
rename from src/components/index.ts
rename to src/components/shared_ui/desktop-wrapper/desktop-wrapper.scss
diff --git a/src/components/shared_ui/desktop-wrapper/desktop-wrapper.tsx b/src/components/shared_ui/desktop-wrapper/desktop-wrapper.tsx
new file mode 100644
index 00000000..cc4ff4bd
--- /dev/null
+++ b/src/components/shared_ui/desktop-wrapper/desktop-wrapper.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { isDesktop } from '@/components/shared/utils/screen';
+
+type TDesktopProps = {
+ children: React.ReactNode;
+};
+
+const Desktop = ({ children }: TDesktopProps) => {
+ if (!isDesktop()) return null;
+ return {children} ;
+};
+
+export default Desktop;
diff --git a/src/components/shared_ui/desktop-wrapper/index.ts b/src/components/shared_ui/desktop-wrapper/index.ts
new file mode 100644
index 00000000..3293a225
--- /dev/null
+++ b/src/components/shared_ui/desktop-wrapper/index.ts
@@ -0,0 +1,5 @@
+import DesktopWrapper from './desktop-wrapper';
+
+import './desktop-wrapper.scss';
+
+export default DesktopWrapper;
diff --git a/src/components/shared_ui/div100vh-container/div100vh-container.scss b/src/components/shared_ui/div100vh-container/div100vh-container.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/shared_ui/div100vh-container/div100vh-container.tsx b/src/components/shared_ui/div100vh-container/div100vh-container.tsx
new file mode 100644
index 00000000..426772e5
--- /dev/null
+++ b/src/components/shared_ui/div100vh-container/div100vh-container.tsx
@@ -0,0 +1,53 @@
+/* eslint @typescript-eslint/triple-slash-reference: "off" */
+///
+import React from 'react';
+import Div100vh from 'react-div-100vh';
+
+/* Div100vh is workaround for getting accurate height of 100vh from browsers on mobile,
+ because using normal css vh is not returning correct screen height */
+/* To adjust max-height using calculation when using height: auto (or no rvh units), pass style props and use rvh unit instead vh,
+ e.g - style={{ maxHeight: calc(100rvh - 100px )}}
+ */
+/* To adjust height using calculation, pass style props and use rvh unit instead vh,
+ e.g - style={{ height: calc(100rvh - 100px )}}
+*/
+/* To manually remove rvh calculation and revert to default browser calculation use is_disabled */
+/* To bypass usage of component altogether, use is_bypassed */
+
+type TDiv100vhContainer = {
+ height_offset?: string;
+ is_bypassed?: boolean;
+ is_disabled?: boolean;
+ max_height_offset?: string;
+ className?: string;
+ max_autoheight_offset?: string;
+ id?: string;
+} & React.ComponentProps<'div'>;
+
+const Div100vhContainer = ({
+ children,
+ className,
+ is_bypassed = false,
+ is_disabled = false,
+ id,
+ height_offset,
+ max_autoheight_offset,
+}: React.PropsWithChildren) => {
+ const height_rule = height_offset ? `calc(100rvh - ${height_offset})` : 'calc(100rvh)';
+ const height_style = {
+ height: max_autoheight_offset ? '' : height_rule,
+ maxHeight: max_autoheight_offset ? `calc(100rvh - ${max_autoheight_offset})` : '',
+ };
+ React.useEffect(() => {
+ // forcing resize event to make Div100vh re-render when height_offset has changed:
+ window.dispatchEvent(new Event('resize'));
+ }, [height_offset]);
+ if (is_bypassed) return children as JSX.Element;
+ return (
+
+ {children}
+
+ );
+};
+
+export default Div100vhContainer;
diff --git a/src/components/shared_ui/div100vh-container/index.ts b/src/components/shared_ui/div100vh-container/index.ts
new file mode 100644
index 00000000..53da6fe0
--- /dev/null
+++ b/src/components/shared_ui/div100vh-container/index.ts
@@ -0,0 +1,5 @@
+import Div100vhContainer from './div100vh-container';
+
+import './div100vh-container.scss';
+
+export default Div100vhContainer;
diff --git a/src/components/shared_ui/drawer/drawer.scss b/src/components/shared_ui/drawer/drawer.scss
new file mode 100644
index 00000000..1d9cbaac
--- /dev/null
+++ b/src/components/shared_ui/drawer/drawer.scss
@@ -0,0 +1,114 @@
+/** @define dc-drawer */
+// TODO: [fix-dc-bundle] Fix import issue with Deriv Component stylesheets (app should take precedence, and not repeat)
+.dc-drawer {
+ $toggler-width: 16px;
+
+ position: fixed;
+ will-change: transform;
+ transition: transform 0.3s ease;
+ background: var(--general-main-2);
+
+ &--left {
+ display: flex;
+ flex-direction: row-reverse;
+ right: calc(100vw - 16px);
+ }
+ &--right {
+ display: flex;
+ flex-direction: row;
+ left: calc(100vw - 16px);
+ }
+
+ @include mobile {
+ width: 100vw !important;
+ height: calc(100% - 10.1rem) !important;
+ top: calc(100% - 9.8rem) !important;
+ }
+ &__container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+
+ &--left {
+ padding-right: $toggler-width;
+ }
+ &--right {
+ padding-left: $toggler-width;
+ }
+
+ @include mobile {
+ height: calc(100% - 3.6rem);
+ }
+ }
+ &__header {
+ border-bottom: 1px solid var(--general-section-1);
+ line-height: 40px;
+ font-weight: 700;
+ font-size: 1.6em;
+ }
+ &__content {
+ flex-grow: 1;
+ overflow: auto;
+ }
+ &__footer {
+ background-color: var(--general-main-2);
+ border-top: 1px solid var(--general-section-1);
+ line-height: 40px;
+ font-weight: 700;
+ font-size: 1.6em;
+ align-items: center;
+ }
+ &__toggle {
+ align-items: center;
+ border-left: 1px solid var(--border-disabled);
+ border-right: 1px solid var(--border-disabled);
+ display: flex;
+ position: absolute;
+ width: $toggler-width;
+ height: 100%;
+ background-color: var(--general-section-5);
+ cursor: pointer;
+
+ @include mobile {
+ position: unset;
+ width: 100%;
+ height: 3.6rem;
+ justify-content: center;
+ background-color: var(--general-main-1);
+ border-top: solid 1px var(--general-section-1);
+ }
+ &-icon {
+ transition: 0.25s ease;
+ &--left {
+ transform: rotate(0);
+ }
+ &--right {
+ transform: rotate(180deg);
+ }
+
+ @include mobile {
+ width: 2.5rem;
+ height: 0.8rem;
+ transform: rotate(0);
+ }
+ }
+ &--open > &-icon {
+ &--left {
+ transform: rotate(180deg);
+ }
+ &--right {
+ transform: rotate(0);
+ }
+
+ @include mobile {
+ transform: rotate(180deg);
+ }
+ }
+ }
+ &--open {
+ @include mobile {
+ transform: translateY(calc(-100% + 3.6rem));
+ }
+ }
+}
diff --git a/src/components/shared_ui/drawer/drawer.tsx b/src/components/shared_ui/drawer/drawer.tsx
new file mode 100644
index 00000000..f970e029
--- /dev/null
+++ b/src/components/shared_ui/drawer/drawer.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { isTabletDrawer } from '@/components/shared';
+import { Icon } from '@/utils/tmp/dummy';
+
+type TDrawer = {
+ anchor?: string;
+ className?: string;
+ contentClassName?: string;
+ footer?: React.ReactElement;
+ header?: React.ReactElement;
+ width?: number;
+ zIndex?: number;
+ is_open: boolean;
+ toggleDrawer?: (prop: boolean) => void;
+};
+
+// TODO: use-from-shared - Use this icon from icons' shared package
+const IconDrawer = ({ className }: { className?: string }) => (
+
+
+
+);
+
+const Drawer = ({
+ anchor = 'left',
+ children,
+ className,
+ contentClassName,
+ footer,
+ header,
+ width = 250,
+ zIndex = 4,
+ ...props
+}: React.PropsWithChildren) => {
+ const [is_open, setIsOpen] = React.useState(props.is_open);
+
+ React.useEffect(() => {
+ setIsOpen(props.is_open);
+ }, [props.is_open]);
+
+ const toggleDrawer = () => {
+ setIsOpen(!is_open);
+ if (props.toggleDrawer) {
+ props.toggleDrawer(!is_open);
+ }
+ };
+
+ // Note: Cannot use isMobile from @deriv/shared due to dimension change error
+ // TODO: Maybe we can fix isMobile in @deriv/shared
+ const is_mobile = isTabletDrawer();
+
+ return (
+
+
+ {is_mobile ? (
+
+ ) : (
+
+ )}
+
+
+ {header &&
{header}
}
+
{children}
+ {footer &&
{footer}
}
+
+
+ );
+};
+
+export default Drawer;
diff --git a/src/components/shared_ui/drawer/index.ts b/src/components/shared_ui/drawer/index.ts
new file mode 100644
index 00000000..cc782901
--- /dev/null
+++ b/src/components/shared_ui/drawer/index.ts
@@ -0,0 +1,5 @@
+import Drawer from './drawer';
+
+import './drawer.scss';
+
+export default Drawer;
diff --git a/src/components/shared_ui/dropdown-list/dropdown-list.scss b/src/components/shared_ui/dropdown-list/dropdown-list.scss
new file mode 100644
index 00000000..2af206bc
--- /dev/null
+++ b/src/components/shared_ui/dropdown-list/dropdown-list.scss
@@ -0,0 +1,64 @@
+.dc-dropdown-list {
+ position: absolute;
+ margin-top: 4px;
+ border-radius: 4px;
+ z-index: 1;
+ box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.24);
+ transform-origin: top;
+ transition:
+ transform 0.25s ease,
+ opacity 0.25s linear;
+ transform: scale(1, 0);
+ cursor: pointer;
+ background-color: var(--state-normal);
+
+ &--enter,
+ &--exit {
+ transform: scale(1, 0);
+ opacity: 0;
+ }
+ &--enter-done {
+ transform: scale(1, 1);
+ opacity: 1;
+ }
+ &__group {
+ &-header {
+ padding: 0.6rem;
+ font-size: 1.4rem;
+ font-weight: bold;
+ color: var(--brand-red-coral);
+ height: 3.6rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+ &__item {
+ min-height: 40px;
+ display: flex;
+ align-items: center;
+ padding: 1rem 1.6rem;
+ font-size: 1.4rem;
+ color: var(--text-general);
+
+ &:hover {
+ background-color: var(--state-hover);
+ }
+ &--active {
+ background-color: var(--state-active);
+ }
+ &-icon {
+ margin-right: 8px;
+ }
+ &--disabled {
+ color: var(--text-disabled);
+ cursor: not-allowed;
+ }
+ }
+ &__separator {
+ display: flex;
+ width: calc(100% - 3.2rem);
+ border-bottom: 1px solid var(--border-normal);
+ margin: 1.6rem;
+ }
+}
diff --git a/src/components/shared_ui/dropdown-list/dropdown-list.tsx b/src/components/shared_ui/dropdown-list/dropdown-list.tsx
new file mode 100644
index 00000000..1fd4fdf2
--- /dev/null
+++ b/src/components/shared_ui/dropdown-list/dropdown-list.tsx
@@ -0,0 +1,225 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+
+import { ResidenceList } from '@deriv/api-types';
+
+import ThemedScrollbars from '../themed-scrollbars/themed-scrollbars';
+
+export type TItem =
+ | string
+ | (ResidenceList[0] & {
+ component?: React.ReactNode;
+ group?: string;
+ text?: string;
+ value?: string;
+ });
+
+type TListItem = {
+ is_active: boolean;
+ is_disabled?: boolean;
+ index: number;
+ item: TItem;
+ child_ref: React.LegacyRef;
+ onItemSelection: (item: TItem) => void;
+ is_object_list?: boolean;
+ setActiveIndex: (index: number) => void;
+};
+
+type TListItems = {
+ active_index: number;
+ is_object_list?: boolean;
+ list_items: TItem[];
+ not_found_text?: string;
+ onItemSelection: (item: TItem) => void;
+ setActiveIndex: (index: number) => void;
+};
+
+type TDropdownRefs = {
+ dropdown_ref: React.RefObject | null;
+ list_item_ref?: React.RefObject;
+ list_wrapper_ref: React.RefObject;
+};
+
+type TDropDownList = {
+ active_index: number;
+ is_visible: boolean;
+ list_items: TItem[];
+ list_height: string;
+ onScrollStop?: React.UIEventHandler;
+ onItemSelection: (item: TItem) => void;
+ setActiveIndex: (index: number) => void;
+ style: React.CSSProperties;
+ not_found_text?: string;
+ portal_id?: string;
+ dropdown_refs?: TDropdownRefs;
+};
+
+const ListItem = ({ is_active, is_disabled, index, item, child_ref, onItemSelection, setActiveIndex }: TListItem) => {
+ return (
+ {
+ event.stopPropagation();
+ onItemSelection(item);
+ setActiveIndex(index);
+ }}
+ className={classNames('dc-dropdown-list__item', {
+ 'dc-dropdown-list__item--active': is_active,
+ 'dc-dropdown-list__item--disabled': is_disabled,
+ })}
+ >
+ {typeof item === 'object' ? item.component || item.text : item}
+
+ );
+};
+
+const ListItems = React.forwardRef((props, ref) => {
+ const { active_index, list_items, is_object_list, onItemSelection, setActiveIndex, not_found_text } = props;
+ const is_grouped_list = list_items?.some(list_item => typeof list_item === 'object' && !!list_item.group);
+
+ if (is_grouped_list) {
+ const groups: { [key: string]: TItem[] } = {};
+
+ list_items.forEach(list_item => {
+ const group = (typeof list_item === 'object' && list_item.group) || '?';
+ if (!groups[group]) {
+ groups[group] = [];
+ }
+ groups[group].push(list_item);
+ });
+
+ const group_names = Object.keys(groups);
+ let item_idx = -1;
+
+ return (
+ <>
+ {group_names.map((group_name, group_idx) => {
+ const group = groups[group_name];
+ const has_separator = !!group_names[group_idx + 1];
+ return (
+
+ {group_name}
+ {group.map(item => {
+ item_idx++;
+ return (
+
+ );
+ })}
+ {has_separator &&
}
+
+ );
+ })}
+ >
+ );
+ }
+
+ return (
+ <>
+ {list_items?.length ? (
+ list_items.map((item, item_idx) => (
+
+ ))
+ ) : (
+ {not_found_text}
+ )}
+ >
+ );
+});
+ListItems.displayName = 'ListItems';
+
+const DropdownList = (props: TDropDownList) => {
+ const { dropdown_ref, list_item_ref, list_wrapper_ref } = props.dropdown_refs || {};
+ const {
+ active_index,
+ is_visible,
+ list_items,
+ list_height,
+ onScrollStop,
+ onItemSelection,
+ setActiveIndex,
+ style,
+ not_found_text,
+ portal_id,
+ } = props;
+
+ if (list_items?.length && typeof list_items[0] !== 'string' && typeof list_items[0] !== 'object') {
+ throw Error('Dropdown received wrong data structure');
+ }
+
+ const is_object = !Array.isArray(list_items) && typeof list_items === 'object';
+ const is_string_array = list_items?.length && typeof list_items[0] === 'string';
+
+ const el_dropdown_list = (
+
+
+
+ {is_object ? (
+ Object.keys(list_items).map((items, idx) => (
+
+ ))
+ ) : (
+
+ )}
+
+
+
+ );
+
+ if (portal_id) {
+ const container = document.getElementById(portal_id);
+ return container && ReactDOM.createPortal(el_dropdown_list, container);
+ }
+ return el_dropdown_list;
+};
+
+DropdownList.displayName = 'DropdownList';
+
+export default DropdownList;
diff --git a/src/components/shared_ui/dropdown-list/index.ts b/src/components/shared_ui/dropdown-list/index.ts
new file mode 100644
index 00000000..31bd9499
--- /dev/null
+++ b/src/components/shared_ui/dropdown-list/index.ts
@@ -0,0 +1,6 @@
+import DropdownList, { TItem } from './dropdown-list';
+
+import './dropdown-list.scss';
+
+export type { TItem };
+export default DropdownList;
diff --git a/src/components/shared_ui/expansion-panel/array-renderer.tsx b/src/components/shared_ui/expansion-panel/array-renderer.tsx
new file mode 100644
index 00000000..9f4f7b2c
--- /dev/null
+++ b/src/components/shared_ui/expansion-panel/array-renderer.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import { TItem } from '../types/common.types';
+
+type TArrayRenderer = {
+ array: Array;
+ open_ids: Array;
+ setOpenIds: (props: Array) => void;
+};
+
+const ArrayRenderer = ({ array, open_ids, setOpenIds }: TArrayRenderer) => {
+ const onArrayItemClick = (id: string) => {
+ if (open_ids.includes(id)) {
+ setOpenIds(open_ids.filter(open_id => open_id !== id));
+ } else {
+ setOpenIds([...open_ids, id]);
+ }
+ };
+
+ return (
+
+ {array.map((item, index) => {
+ if (Array.isArray(item?.value)) {
+ return (
+
+
+ {`${index + 1}: `}
+ ({`${item.value.length}`})
+ onArrayItemClick(item.id)}
+ />
+
+ {open_ids.includes(item.id) ? (
+
+ ) : null}
+
+ );
+ }
+ return (
+
+ {`${index + 1}: `}
+ {item?.value?.toString()}
+
+ );
+ })}
+
+ );
+};
+
+export default ArrayRenderer;
diff --git a/src/components/shared_ui/expansion-panel/expansion-panel.scss b/src/components/shared_ui/expansion-panel/expansion-panel.scss
new file mode 100644
index 00000000..c03c59d1
--- /dev/null
+++ b/src/components/shared_ui/expansion-panel/expansion-panel.scss
@@ -0,0 +1,43 @@
+.dc-expansion-panel {
+ &__header {
+ &-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-weight: bold;
+ }
+ &-chevron-icon {
+ cursor: pointer;
+ transition: transform 0.3s ease;
+ transform: rotate(0deg);
+ }
+ &-active &-chevron-icon {
+ transform: rotate(180deg);
+ }
+ &-active {
+ margin-bottom: 1.6rem;
+ }
+ }
+ &__content {
+ &-array {
+ display: flex;
+ align-items: flex-start;
+ justify-content: flex-start;
+ font-size: var(--text-size-xxs);
+ line-height: 1.5;
+ }
+ &-chevron-icon {
+ margin-left: 4px;
+ margin-right: 4px;
+ cursor: pointer;
+ transition: transform 0.3s ease;
+ transform: rotate(0deg);
+ }
+ &-active &-chevron-icon {
+ transform: rotate(90deg);
+ }
+ &-array-item-index {
+ margin-right: 4px;
+ }
+ }
+}
diff --git a/src/components/shared_ui/expansion-panel/expansion-panel.tsx b/src/components/shared_ui/expansion-panel/expansion-panel.tsx
new file mode 100644
index 00000000..fae9a56b
--- /dev/null
+++ b/src/components/shared_ui/expansion-panel/expansion-panel.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import { TItem } from '../types/common.types';
+
+import ArrayRenderer from './array-renderer';
+
+type TExpansionPanel = {
+ message: { header: React.ReactNode; content: (Array & Array) | React.ReactNode };
+ onResize?: () => void;
+};
+
+const ExpansionPanel = ({ message, onResize }: TExpansionPanel) => {
+ const [open_ids, setOpenIds] = React.useState([]);
+ const [is_open, setIsOpen] = React.useState(false);
+
+ React.useEffect(() => {
+ if (typeof onResize === 'function') {
+ onResize();
+ }
+ }, [is_open, onResize]);
+
+ const onClick = () => {
+ // close if clicking the expansion panel that's open, otherwise open the new one
+ setIsOpen(!is_open);
+ };
+
+ return (
+
+
+ {message.header}
+
+
+ {is_open &&
+ (Array.isArray(message.content) ? (
+
+ ) : (
+ message.content
+ ))}
+
+ );
+};
+
+export default ExpansionPanel;
diff --git a/src/components/shared_ui/expansion-panel/index.ts b/src/components/shared_ui/expansion-panel/index.ts
new file mode 100644
index 00000000..05384893
--- /dev/null
+++ b/src/components/shared_ui/expansion-panel/index.ts
@@ -0,0 +1,5 @@
+import ExpansionPanel from './expansion-panel';
+
+import './expansion-panel.scss';
+
+export default ExpansionPanel;
diff --git a/src/components/shared_ui/fade-wrapper/fade-wrapper.scss b/src/components/shared_ui/fade-wrapper/fade-wrapper.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/shared_ui/fade-wrapper/fade-wrapper.tsx b/src/components/shared_ui/fade-wrapper/fade-wrapper.tsx
new file mode 100644
index 00000000..0548f982
--- /dev/null
+++ b/src/components/shared_ui/fade-wrapper/fade-wrapper.tsx
@@ -0,0 +1,102 @@
+import { ReactNode } from 'react';
+import { motion } from 'framer-motion';
+
+type TFadeWrapperProps = {
+ children: ReactNode;
+ is_visible: boolean;
+ keyname?: string;
+ type?: 'top' | 'bottom';
+ className?: string;
+};
+
+const FadeInFromTopDiv = {
+ initial: {
+ y: -50,
+ opacity: 0,
+ },
+ animate: {
+ y: 0,
+ opacity: 1,
+ },
+
+ transition: { duration: 250, delay: 0.3 },
+};
+
+const FadeInFromBottomDiv = {
+ initial: {
+ y: 50,
+ opacity: 0,
+ },
+ animate: {
+ y: 0,
+ opacity: 1,
+ },
+
+ transition: { duration: 0.25, delay: 0.3 },
+};
+
+const FadeInOnlyDiv = {
+ initial: {
+ opacity: 0,
+ },
+ animate: {
+ opacity: 1,
+ },
+
+ transition: { duration: 0.3 },
+};
+
+// `flipMove={false}` is necessary to fix react-pose bug: https://github.com/Popmotion/popmotion/issues/805
+const FadeWrapper = ({ children, className, is_visible, keyname, type }: TFadeWrapperProps) => {
+ if (type === 'top') {
+ return (
+ <>
+ {is_visible && (
+
+ {children}
+
+ )}
+ >
+ );
+ }
+ if (type === 'bottom') {
+ return (
+ <>
+ {is_visible && (
+
+ {children}
+
+ )}
+ >
+ );
+ }
+ return (
+ <>
+ {is_visible && (
+
+ {children}
+
+ )}
+ >
+ );
+};
+
+export default FadeWrapper;
diff --git a/src/components/shared_ui/fade-wrapper/index.ts b/src/components/shared_ui/fade-wrapper/index.ts
new file mode 100644
index 00000000..2489c54e
--- /dev/null
+++ b/src/components/shared_ui/fade-wrapper/index.ts
@@ -0,0 +1,5 @@
+import FadeWrapper from './fade-wrapper';
+
+import './fade-wrapper.scss';
+
+export default FadeWrapper;
diff --git a/src/components/shared_ui/field/field.scss b/src/components/shared_ui/field/field.scss
new file mode 100644
index 00000000..055627c5
--- /dev/null
+++ b/src/components/shared_ui/field/field.scss
@@ -0,0 +1,26 @@
+.dc-field {
+ position: absolute;
+ top: 4.1rem;
+ text-align: left !important;
+ font-size: var(--text-size-xxs);
+ line-height: 1.25;
+
+ &--error {
+ padding-left: 1.1rem;
+ @include typeface(--small-left-normal-red);
+ line-height: 1;
+ }
+ &--warn {
+ padding-left: 1.2rem;
+ padding-top: 0.4rem;
+ @include typeface(--small-left-normal-black);
+ color: $color-grey-1;
+ }
+}
+
+.dc-input__footer {
+ .dc-field {
+ position: relative;
+ top: unset;
+ }
+}
diff --git a/src/components/shared_ui/field/field.tsx b/src/components/shared_ui/field/field.tsx
new file mode 100644
index 00000000..6d87235b
--- /dev/null
+++ b/src/components/shared_ui/field/field.tsx
@@ -0,0 +1,20 @@
+import classNames from 'classnames';
+
+type TFieldProps = {
+ className?: string;
+ message: string;
+ type?: 'error' | 'warn';
+};
+
+const Field = ({ message, className, type }: TFieldProps) => (
+
+ {message}
+
+);
+
+export default Field;
diff --git a/src/components/shared_ui/field/index.ts b/src/components/shared_ui/field/index.ts
new file mode 100644
index 00000000..8ae751ff
--- /dev/null
+++ b/src/components/shared_ui/field/index.ts
@@ -0,0 +1,5 @@
+import Field from './field';
+
+import './field.scss';
+
+export default Field;
diff --git a/src/components/shared_ui/input-field/increment-buttons.tsx b/src/components/shared_ui/input-field/increment-buttons.tsx
new file mode 100644
index 00000000..5d04b733
--- /dev/null
+++ b/src/components/shared_ui/input-field/increment-buttons.tsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Button } from '@deriv-com/ui';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+export type TButtonType = 'button' | 'submit' | 'reset';
+
+type IncrementButtonsProps = {
+ decrementValue: (
+ ev?: React.MouseEvent | React.TouchEvent,
+ long_press_step?: number
+ ) => void;
+ id?: string;
+ incrementValue: (
+ ev?: React.MouseEvent | React.TouchEvent,
+ long_press_step?: number
+ ) => void;
+ onLongPressEnd: () => void;
+ is_incrementable_on_long_press?: boolean;
+ max_is_disabled: number | boolean;
+ min_is_disabled: number | boolean;
+ type?: TButtonType;
+};
+
+const IncrementButtons = ({
+ decrementValue,
+ id,
+ incrementValue,
+ max_is_disabled,
+ min_is_disabled,
+ is_incrementable_on_long_press,
+ onLongPressEnd,
+ type,
+}: IncrementButtonsProps) => {
+ const interval_ref = React.useRef>();
+ const timeout_ref = React.useRef>();
+ const is_long_press_ref = React.useRef(false);
+
+ const handleButtonPress =
+ (
+ onChange: (
+ e: React.TouchEvent | React.MouseEvent,
+ step: number
+ ) => void
+ ) =>
+ (ev: React.TouchEvent | React.MouseEvent) => {
+ timeout_ref.current = setTimeout(() => {
+ is_long_press_ref.current = true;
+ let step = 1;
+ onChange(ev, step);
+ interval_ref.current = setInterval(() => {
+ onChange(ev, ++step);
+ }, 50);
+ }, 300);
+ };
+
+ const handleButtonRelease = () => {
+ clearInterval(interval_ref.current);
+ clearTimeout(timeout_ref.current);
+
+ if (onLongPressEnd && is_long_press_ref.current) onLongPressEnd();
+
+ is_long_press_ref.current = false;
+ };
+
+ const getPressEvents = (
+ onChange: (e: React.TouchEvent | React.MouseEvent, step: number) => void
+ ) => {
+ if (!is_incrementable_on_long_press) return {};
+ return {
+ onContextMenu: (e: React.TouchEvent | React.MouseEvent) =>
+ e.preventDefault(),
+ onTouchStart: handleButtonPress(onChange),
+ onTouchEnd: handleButtonRelease,
+ onMouseDown: handleButtonPress(onChange),
+ onMouseUp: handleButtonRelease,
+ };
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default IncrementButtons;
diff --git a/src/components/shared_ui/input-field/index.ts b/src/components/shared_ui/input-field/index.ts
new file mode 100644
index 00000000..9d61e88a
--- /dev/null
+++ b/src/components/shared_ui/input-field/index.ts
@@ -0,0 +1,5 @@
+import InputField from './input-field';
+
+import './input-field.scss';
+
+export default InputField;
diff --git a/src/components/shared_ui/input-field/input-field.scss b/src/components/shared_ui/input-field/input-field.scss
new file mode 100644
index 00000000..2ba5a21a
--- /dev/null
+++ b/src/components/shared_ui/input-field/input-field.scss
@@ -0,0 +1,186 @@
+/** @define dc-input-field; weak */
+.dc-input-field {
+ margin: 0.8rem 0 0;
+ position: relative;
+
+ &__label {
+ @include typeface(--paragraph-left-normal-black);
+ color: var(--text-general);
+ margin-bottom: 0.2rem;
+ display: inline-block;
+ text-align: center;
+ width: 100%;
+ }
+ & .inline-icon {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ z-index: 1;
+ cursor: pointer;
+ }
+ &__helper {
+ @include typeface(--small-left-light-grey);
+ }
+}
+
+/** @define dc-input-wrapper */
+.dc-input-wrapper,
+button.dc-input-wrapper {
+ position: relative;
+
+ &__input {
+ &::-ms-clear {
+ // Edge: hide clear icon for incement input
+ display: none;
+ }
+ }
+ &__button {
+ position: absolute !important;
+ z-index: 1;
+ top: 2px;
+ height: 28px;
+ border: none;
+ padding: 0;
+ margin: initial;
+ min-width: 3.2rem;
+ border-radius: 3px;
+ background-color: initial;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &--increment {
+ /* rtl:ignore */
+ right: 2px;
+ }
+ &--decrement {
+ /* rtl:ignore */
+ left: 2px;
+ }
+ &:hover {
+ background: var(--state-hover);
+ cursor: pointer;
+
+ & ~ .dc-input-wrapper__input {
+ @extend .input-hover;
+ }
+ }
+ &:disabled:hover {
+ cursor: not-allowed;
+ background: transparent;
+ }
+ }
+ &__icon {
+ // TODO: fix check if necessary
+ // @extend %inline-icon;
+ }
+ &--disabled {
+ pointer-events: none;
+ color: var(--text-disabled);
+ }
+ &--inline {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+}
+
+/** @define input */
+.input {
+ @include typeface(--paragraph-center-normal-black);
+ list-style: none;
+ position: relative;
+ display: inline-block;
+ width: 100%;
+ height: 32px;
+ padding-left: 0.8rem;
+ border-radius: $BORDER_RADIUS;
+ transition: transform 0.3s;
+ background-color: var(--fill-normal);
+ border: 1px solid var(--fill-normal);
+ color: var(--text-prominent);
+
+ &[type='checkbox'] {
+ font-size: 1.2rem;
+ cursor: pointer;
+ display: inline-block;
+ vertical-align: middle;
+ width: 16px;
+ height: 16px;
+ border-radius: 0;
+ padding: 0;
+ background-color: var(--general-section-1);
+ border: 1px solid var(--text-less-prominent);
+
+ &:active {
+ border-radius: 0;
+ box-shadow: none;
+ border-color: var(--brand-red-coral);
+ }
+ &:hover {
+ border-color: var(--border-hover);
+ }
+ &:focus {
+ border-radius: 0;
+ box-shadow: none;
+ border-color: var(--border-active);
+ }
+ &:checked {
+ background: var(--brand-red-coral);
+ border-color: var(--brand-red-coral);
+
+ &:after {
+ display: inline-block;
+ }
+ &:hover {
+ border-color: var(--brand-red-coral);
+ }
+ }
+ &::-ms-check {
+ // IE/EDGE support
+ background: transparent;
+ border-color: transparent;
+ color: $COLOR_WHITE;
+ }
+ &:after {
+ @include typeface(--small-left-normal-active);
+ content: 'L';
+ transform: scaleX(-1) rotate(-40deg);
+ position: absolute;
+ top: -10%;
+ left: 25%;
+ display: none;
+ }
+ }
+ &:not([type='range']) {
+ touch-action: manipulation;
+ }
+ &[readonly] {
+ cursor: pointer;
+ }
+ &::placeholder {
+ border-color: var(--border-normal);
+ }
+ &:hover,
+ &-hover {
+ border-color: var(--border-hover);
+ }
+ &:active,
+ &:focus {
+ outline: 0;
+ border-radius: $BORDER_RADIUS;
+ border-color: var(--border-active);
+ color: var(--text-prominent);
+ }
+ &--has-inline-prefix {
+ padding-right: 40px !important;
+ border-radius: $BORDER_RADIUS !important;
+ }
+ &--error {
+ color: $COLOR_RED;
+ border: 1px solid $COLOR_RED !important;
+ }
+ &:disabled {
+ color: var(--text-disabled);
+ }
+}
diff --git a/src/components/shared_ui/input-field/input-field.tsx b/src/components/shared_ui/input-field/input-field.tsx
new file mode 100644
index 00000000..973d05d8
--- /dev/null
+++ b/src/components/shared_ui/input-field/input-field.tsx
@@ -0,0 +1,407 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { getCurrencyDisplayCode, isCryptocurrency } from '@/components/shared';
+
+import Text from '../text';
+import Tooltip from '../tooltip';
+
+import IncrementButtons, { TButtonType } from './increment-buttons';
+import Input, { TInputMode } from './input';
+
+export type TChangeEvent = { target: { name: string; value: number | string } };
+
+// ToDo: Refactor input_field
+// supports more than two different types of 'value' as a prop.
+// Quick Solution - Pass two different props to input field.
+type TInputField = {
+ ariaLabel?: string;
+ checked?: boolean;
+ className?: string;
+ classNameDynamicSuffix?: string;
+ classNameInlinePrefix?: string;
+ classNameInput?: string;
+ classNamePrefix?: string;
+ classNameWrapper?: string; // CSS class for the component wrapper
+ currency?: string;
+ current_focus?: string | null;
+ data_testid?: string;
+ data_tip?: string;
+ data_value?: string;
+ decimal_point_change?: number; // Specify which decimal point must be updated when the increment/decrement button is pressed
+ error_message_alignment?: string;
+ error_messages?: string[];
+ format?: (new_value?: string) => string;
+ fractional_digits?: number;
+ helper?: string;
+ icon?: React.ElementType;
+ id?: string;
+ increment_button_type?: TButtonType;
+ inline_prefix?: string;
+ inputmode?: TInputMode;
+ is_autocomplete_disabled?: boolean;
+ is_disabled?: boolean;
+ is_error_tooltip_hidden?: boolean;
+ is_float?: boolean;
+ is_hj_whitelisted?: boolean;
+ is_incrementable_on_long_press?: boolean;
+ is_incrementable?: boolean;
+ is_negative_disabled?: boolean;
+ is_read_only?: boolean;
+ is_signed?: boolean;
+ is_unit_at_right?: boolean;
+ label?: string;
+ max_length?: number;
+ max_value?: number;
+ min_value?: number;
+ name?: string;
+ onBlur?: React.FocusEventHandler;
+ onChange?: (e: TChangeEvent) => void;
+ onClick?: React.MouseEventHandler;
+ onClickInputWrapper?: React.MouseEventHandler;
+ placeholder?: string;
+ prefix?: string;
+ required?: boolean;
+ setCurrentFocus?: (name: string | null) => void;
+ type?: string;
+ unit?: string;
+ value: number | string;
+};
+
+const InputField = ({
+ ariaLabel,
+ checked,
+ className,
+ classNameDynamicSuffix,
+ classNameInlinePrefix,
+ classNameInput,
+ classNamePrefix,
+ classNameWrapper,
+ currency,
+ current_focus,
+ data_tip,
+ data_value,
+ decimal_point_change,
+ error_messages,
+ error_message_alignment,
+ fractional_digits,
+ helper,
+ icon,
+ id,
+ inline_prefix,
+ is_autocomplete_disabled,
+ is_disabled,
+ is_error_tooltip_hidden = false,
+ is_float,
+ is_hj_whitelisted = false,
+ is_incrementable,
+ is_incrementable_on_long_press,
+ is_negative_disabled,
+ is_read_only = false,
+ is_signed = false,
+ is_unit_at_right = false,
+ inputmode,
+ increment_button_type,
+ label,
+ max_length,
+ max_value,
+ min_value,
+ name = '',
+ format,
+ onBlur,
+ onChange,
+ onClick,
+ onClickInputWrapper,
+ placeholder,
+ prefix,
+ required,
+ setCurrentFocus,
+ type = '',
+ unit,
+ value,
+ data_testid,
+}: TInputField) => {
+ const [local_value, setLocalValue] = React.useState();
+ const Icon = icon as React.ElementType;
+ const has_error = error_messages && !!error_messages.length && !is_error_tooltip_hidden;
+ const max_is_disabled = max_value && (+value >= +max_value || Number(local_value) >= +max_value);
+ const min_is_disabled = min_value && (+value <= +min_value || Number(local_value) <= +min_value);
+ let has_valid_length = true;
+
+ const changeValue = (
+ e: React.ChangeEvent,
+ callback?: (evt: React.ChangeEvent) => void
+ ) => {
+ if (unit) {
+ e.target.value = e.target.value.replace(unit, '').trim();
+ }
+
+ if (e.target.value === value && type !== 'checkbox') {
+ return;
+ }
+
+ if (type === 'number' || type === 'tel') {
+ const is_empty = !e.target.value || e.target.value === '' || e.target.value === ' ';
+ const signed_regex = is_signed ? '^([+-.0-9])' : '^';
+ e.target.value = e.target.value.replace(',', '.');
+
+ const is_number = new RegExp(`${signed_regex}(\\d*)?${is_float ? '(\\.\\d+)?' : ''}$`).test(e.target.value);
+
+ const is_not_completed_number =
+ is_float && new RegExp(`${signed_regex}(\\.|\\d+\\.)?$`).test(e.target.value);
+ // This regex check whether there is any zero at the end of fractional part or not.
+ const has_zero_at_end = new RegExp(`${signed_regex}(\\d+)?\\.(\\d+)?[0]+$`).test(e.target.value);
+
+ const is_scientific_notation = /e/.test(`${+e.target.value}`);
+
+ if (max_length && (fractional_digits || fractional_digits === 0)) {
+ has_valid_length = new RegExp(
+ `${signed_regex}(\\d{0,${max_length}})(\\${fractional_digits && '.'}\\d{0,${fractional_digits}})?$`
+ ).test(e.target.value);
+ }
+
+ if ((is_number || is_empty) && has_valid_length) {
+ (e.target.value as string | number) =
+ is_empty || is_signed || has_zero_at_end || is_scientific_notation || type === 'tel'
+ ? e.target.value
+ : +e.target.value;
+ } else if (!is_not_completed_number) {
+ (e.target.value as string | number) = value;
+ return;
+ }
+ }
+
+ onChange?.(e);
+ if (callback) {
+ callback(e);
+ }
+ };
+
+ const getDecimals = (val: string | number) => {
+ const array_value = typeof val === 'string' ? val.split('.') : val.toString().split('.');
+ return array_value && array_value.length > 1 ? array_value[1].length : 0;
+ };
+
+ const getClampedValue = (val: number) => {
+ let new_value = val;
+ if (min_value) {
+ new_value = Math.max(new_value, min_value);
+ }
+ if (max_value) {
+ new_value = Math.min(new_value, max_value);
+ }
+ return new_value;
+ };
+
+ const incrementValue = (
+ ev?: React.MouseEvent | React.TouchEvent,
+ long_press_step?: number
+ ) => {
+ if (max_is_disabled) return;
+ let increment_value;
+
+ const current_value = local_value || value.toString();
+
+ const decimal_places = current_value ? getDecimals(current_value) : 0;
+ const is_crypto = !!currency && isCryptocurrency(currency);
+
+ if (long_press_step) {
+ const increase_percentage = Math.min(long_press_step, Math.max(long_press_step, 10)) / 10;
+ const increase = (+value * increase_percentage) / 100;
+ const new_value = parseFloat(current_value || '0') + Math.abs(increase);
+
+ increment_value = parseFloat(getClampedValue(new_value).toString()).toFixed(decimal_places);
+ } else if (is_crypto || (!currency && is_float)) {
+ const new_value =
+ parseFloat(current_value || '0') +
+ parseFloat((1 * 10 ** (0 - (decimal_point_change || decimal_places))).toString());
+ increment_value = parseFloat(new_value.toString()).toFixed(decimal_point_change || decimal_places);
+ } else {
+ increment_value = parseFloat(((+current_value || 0) + 1).toString()).toFixed(decimal_places);
+ }
+
+ updateValue(increment_value, !!long_press_step);
+ };
+
+ const calculateDecrementedValue = (long_press_step?: number) => {
+ let decrement_value;
+ const current_value = local_value || value?.toString();
+
+ const decimal_places = current_value ? getDecimals(current_value) : 0;
+ const is_crypto = !!currency && isCryptocurrency(currency);
+
+ if (long_press_step) {
+ const decrease_percentage = Math.min(long_press_step, Math.max(long_press_step, 10)) / 10;
+ const decrease = (+value * decrease_percentage) / 100;
+ const new_value = parseFloat(current_value || '0') - Math.abs(decrease);
+
+ decrement_value = parseFloat(getClampedValue(new_value).toString()).toFixed(decimal_places);
+ } else if (is_crypto || (!currency && is_float)) {
+ const new_value =
+ parseFloat(current_value || '0') -
+ parseFloat((1 * 10 ** (0 - (decimal_point_change || decimal_places))).toString());
+ decrement_value = parseFloat(new_value.toString()).toFixed(decimal_point_change || decimal_places);
+ } else {
+ decrement_value = parseFloat(((+current_value || 0) - 1).toString()).toFixed(decimal_places);
+ }
+ return decrement_value;
+ };
+
+ const decrementValue = (
+ ev?: React.MouseEvent | React.TouchEvent,
+ long_press_step?: number
+ ) => {
+ if (min_is_disabled) {
+ return;
+ }
+ const decrement_value = calculateDecrementedValue(long_press_step);
+ if (is_negative_disabled && +decrement_value < 0) {
+ return;
+ }
+
+ updateValue(decrement_value, !!long_press_step);
+ };
+
+ const updateValue = (new_value: string, is_long_press: boolean) => {
+ let formatted_value = format ? format(new_value) : new_value;
+ if (is_long_press) {
+ setLocalValue(formatted_value);
+ } else {
+ if (is_signed && /^\d+/.test(formatted_value) && +formatted_value > 0) {
+ formatted_value = `+${formatted_value}`;
+ }
+ onChange?.({ target: { value: formatted_value, name } });
+ }
+ };
+
+ const onLongPressEnd = () => {
+ const new_value = local_value;
+ const formatted_value = format ? format(new_value) : new_value;
+ onChange?.({ target: { value: formatted_value || '', name } });
+
+ setLocalValue('');
+ };
+
+ const onKeyPressed = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 38) incrementValue(); // up-arrow pressed
+ if (e.keyCode === 40) decrementValue(); // down-arrow pressed
+ };
+
+ let display_value = local_value || value;
+
+ if (unit) {
+ display_value = is_unit_at_right ? `${value} ${unit}` : `${unit} ${value}`;
+ }
+
+ const is_increment_input = is_incrementable && (type === 'number' || type === 'tel');
+
+ const input = (
+
+ );
+
+ const increment_buttons = (
+
+ );
+
+ const input_tooltip = (
+
+ {!!label && (
+
+ {label}
+
+ )}
+ {!!helper && (
+
+ {helper}
+
+ )}
+ {is_increment_input ? (
+
+ {increment_buttons}
+ {input}
+
+ ) : (
+ input
+ )}
+
+ );
+
+ return (
+
+ {!!prefix && (
+
+
+ {getCurrencyDisplayCode(currency)}
+
+
+ )}
+
+ {icon && }
+ {input_tooltip}
+
+
+ );
+};
+
+export default InputField;
diff --git a/src/components/shared_ui/input-field/input.tsx b/src/components/shared_ui/input-field/input.tsx
new file mode 100644
index 00000000..10310dbe
--- /dev/null
+++ b/src/components/shared_ui/input-field/input.tsx
@@ -0,0 +1,154 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { getCurrencyDisplayCode } from '@/components/shared';
+
+import Text from '../text';
+
+export type TInputMode = 'search' | 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal';
+type TInputProps = {
+ ariaLabel?: string;
+ changeValue: (
+ e: React.ChangeEvent,
+ callback?: (evt: React.ChangeEvent) => void
+ ) => void;
+ checked?: boolean;
+ className?: string;
+ classNameDynamicSuffix?: string;
+ classNameInlinePrefix?: string;
+ current_focus?: string | null;
+ data_testid?: string;
+ data_tip?: string;
+ data_value?: number | string;
+ display_value: number | string;
+ fractional_digits?: number;
+ has_error?: boolean;
+ id?: string;
+ inline_prefix?: string;
+ inputmode?: TInputMode;
+ is_autocomplete_disabled?: boolean;
+ is_disabled?: boolean;
+ is_hj_whitelisted: boolean;
+ is_incrementable?: boolean;
+ is_read_only: boolean;
+ max_length?: number;
+ name: string;
+ onBlur?: React.FocusEventHandler;
+ onClick?: React.MouseEventHandler;
+ onClickInputWrapper?: React.MouseEventHandler;
+ onKeyPressed: React.KeyboardEventHandler;
+ placeholder?: string;
+ required?: boolean;
+ setCurrentFocus?: (name: string | null) => void;
+ type: string;
+ value?: number | string;
+};
+
+const Input = ({
+ ariaLabel,
+ changeValue,
+ checked,
+ className,
+ classNameDynamicSuffix,
+ classNameInlinePrefix,
+ current_focus,
+ data_testid,
+ data_tip,
+ data_value,
+ display_value,
+ fractional_digits,
+ id,
+ inline_prefix,
+ inputmode,
+ is_autocomplete_disabled,
+ is_disabled,
+ is_hj_whitelisted,
+ is_incrementable,
+ is_read_only,
+ max_length,
+ name,
+ onBlur,
+ onClick,
+ onKeyPressed,
+ placeholder,
+ required,
+ setCurrentFocus,
+ type,
+}: TInputProps) => {
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ if (current_focus === name) {
+ ref?.current?.focus();
+ }
+ }, [current_focus, name]);
+
+ const onBlurHandler = (e: React.FocusEvent) => {
+ setCurrentFocus?.(null);
+ onBlur?.(e);
+ };
+ const onFocus = () => setCurrentFocus?.(name);
+
+ const onChange = (e: React.ChangeEvent) => {
+ /**
+ * fix for Safari
+ * we have to keep track of the current cursor position, update the value in store,
+ * then reset the cursor position to the current cursor position
+ */
+ // TODO: find better ways to target browsers
+ if (navigator.userAgent.indexOf('Safari') !== -1 && type !== 'checkbox') {
+ const cursor = e.target.selectionStart;
+ changeValue(e, evt => {
+ (evt as React.ChangeEvent).target.selectionEnd = cursor; // reset the cursor position in callback
+ });
+ } else {
+ changeValue(e);
+ }
+ };
+
+ return (
+
+ {inline_prefix ? (
+
+
+ {inline_prefix === 'UST' ? getCurrencyDisplayCode(inline_prefix) : inline_prefix}
+
+
+ ) : null}
+
+
+ );
+};
+
+export default Input;
diff --git a/src/components/shared_ui/input-wth-checkbox/index.ts b/src/components/shared_ui/input-wth-checkbox/index.ts
new file mode 100644
index 00000000..c01ed5a1
--- /dev/null
+++ b/src/components/shared_ui/input-wth-checkbox/index.ts
@@ -0,0 +1,5 @@
+import InputWithCheckBox from './input-with-checkbox';
+
+import './input-with-checkbox.scss';
+
+export default InputWithCheckBox;
diff --git a/src/components/shared_ui/input-wth-checkbox/input-with-checkbox.scss b/src/components/shared_ui/input-wth-checkbox/input-with-checkbox.scss
new file mode 100644
index 00000000..591d6938
--- /dev/null
+++ b/src/components/shared_ui/input-wth-checkbox/input-with-checkbox.scss
@@ -0,0 +1,10 @@
+/** @define dc-input-wrapper */
+.dc-input-wrapper {
+ position: relative;
+
+ &--inline {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+}
diff --git a/src/components/shared_ui/input-wth-checkbox/input-with-checkbox.tsx b/src/components/shared_ui/input-wth-checkbox/input-with-checkbox.tsx
new file mode 100644
index 00000000..d74efe12
--- /dev/null
+++ b/src/components/shared_ui/input-wth-checkbox/input-with-checkbox.tsx
@@ -0,0 +1,205 @@
+import React from 'react';
+
+import { getDecimalPlaces, isDesktop, isMobile } from '@/components/shared';
+
+import Checkbox from '../checkbox';
+import InputField from '../input-field';
+import Popover from '../popover';
+import { TToastConfig } from '../types';
+
+type TPosition = 'left' | 'right' | 'top' | 'bottom';
+type TInputWithCheckbox = {
+ addToast: (toast_config: TToastConfig) => void;
+ removeToast: (e: string) => void;
+ checkbox_tooltip_label?: boolean;
+ className?: string;
+ classNameBubble?: string;
+ classNameInlinePrefix?: string;
+ classNameInput?: string;
+ classNamePrefix?: string;
+ currency: string;
+ current_focus?: string | null;
+ defaultChecked: boolean;
+ error_messages?: string[];
+ is_negative_disabled: boolean;
+ is_single_currency: boolean;
+ is_input_hidden?: boolean;
+ label: string;
+ max_value?: number;
+ name: string;
+ onChange: (
+ e: React.ChangeEvent | { target: { name: string; value: number | string | boolean } }
+ ) => void;
+ setCurrentFocus: (name: string | null) => void;
+ tooltip_label?: React.ReactNode;
+ tooltip_alignment?: TPosition;
+ error_message_alignment: string;
+ value: number | string;
+ is_disabled?: boolean;
+};
+const InputWithCheckbox = ({
+ addToast,
+ checkbox_tooltip_label,
+ classNameBubble,
+ classNameInlinePrefix,
+ classNameInput,
+ className,
+ currency,
+ current_focus,
+ defaultChecked,
+ error_message_alignment,
+ error_messages,
+ is_disabled,
+ is_single_currency,
+ is_negative_disabled,
+ is_input_hidden,
+ label,
+ max_value,
+ name,
+ onChange,
+ removeToast,
+ setCurrentFocus,
+ tooltip_alignment,
+ tooltip_label,
+ value,
+}: TInputWithCheckbox) => {
+ const checkboxRef = React.useRef(null);
+ const input_wrapper_ref = React.useRef(null);
+ const [is_checked, setChecked] = React.useState(defaultChecked);
+ const checkboxName = `has_${name}`;
+ React.useEffect(() => {
+ setChecked(defaultChecked);
+ }, [defaultChecked]);
+ // eslint-disable-next-line consistent-return
+ React.useEffect(() => {
+ if (isMobile()) {
+ const showErrorToast = () => {
+ if (typeof addToast === 'function') {
+ addToast({
+ key: `${name}__error`,
+ content: String(error_messages),
+ type: 'error',
+ });
+ }
+ };
+ const removeErrorToast = () => {
+ if (typeof removeToast === 'function') {
+ removeToast(`${name}__error`);
+ }
+ };
+ if (error_messages?.length !== undefined && error_messages?.length > 0) {
+ showErrorToast();
+ return () => {
+ removeErrorToast();
+ };
+ }
+ }
+ }, [error_messages, addToast, removeToast, name]);
+
+ const focusInput = () => {
+ setTimeout(() => {
+ const el_input: HTMLElement | null = (
+ input_wrapper_ref.current?.nextSibling as HTMLElement
+ )?.querySelector?.('input.dc-input-wrapper__input');
+ el_input?.focus?.();
+ });
+ };
+
+ const changeValue = () => {
+ const new_is_checked = !is_checked;
+ // e.target.checked is not reliable, we have to toggle its previous value
+ onChange?.({ target: { name: checkboxName, value: new_is_checked } });
+ if (new_is_checked) focusInput();
+ };
+
+ const enableInputOnClick = () => {
+ if (!is_checked) {
+ setChecked(true);
+ onChange?.({ target: { name: checkboxName, value: true } });
+ focusInput();
+ }
+ };
+
+ const input = (
+ 0}
+ is_hj_whitelisted
+ is_incrementable
+ is_negative_disabled={is_negative_disabled}
+ max_length={10}
+ max_value={max_value}
+ name={name}
+ onChange={onChange}
+ onClickInputWrapper={is_disabled ? undefined : enableInputOnClick}
+ type='number'
+ ariaLabel=''
+ inputmode='decimal'
+ value={value}
+ setCurrentFocus={setCurrentFocus}
+ />
+ );
+
+ const checkbox = (
+
+ );
+
+ return (
+
+
+ {checkbox_tooltip_label ? (
+
+ {checkbox}
+
+ ) : (
+
{checkbox}
+ )}
+ {tooltip_label && (
+
+ )}
+
+ {!is_input_hidden && input}
+
+ );
+};
+
+export default InputWithCheckbox;
diff --git a/src/components/shared_ui/input/index.ts b/src/components/shared_ui/input/index.ts
new file mode 100644
index 00000000..b1a45abb
--- /dev/null
+++ b/src/components/shared_ui/input/index.ts
@@ -0,0 +1,6 @@
+import Input, { TInputProps } from './input';
+
+import './input.scss';
+
+export type { TInputProps };
+export default Input;
diff --git a/src/components/shared_ui/input/input.scss b/src/components/shared_ui/input/input.scss
new file mode 100644
index 00000000..121bd6f4
--- /dev/null
+++ b/src/components/shared_ui/input/input.scss
@@ -0,0 +1,299 @@
+@import './../../shared/styles/constants.scss';
+
+.dc-input {
+ position: relative;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ margin-bottom: 3.2rem;
+ line-height: 1.43;
+
+ &:hover:not(.dc-input--disabled) {
+ border-color: var(--border-hover);
+ }
+ &:focus-within {
+ border-color: var(--brand-secondary);
+
+ &:hover {
+ border-color: var(--brand-secondary);
+ }
+ }
+ &--bottom-label-active {
+ margin-bottom: unset;
+
+ &.dc-input--error {
+ margin-bottom: calc(5rem - 12px);
+ }
+ }
+ &__bottom-label {
+ margin-left: 1.2rem;
+ margin-bottom: calc(3.2rem - 12px);
+ }
+ &--disabled {
+ border-color: var(--border-normal);
+
+ .dc-datepicker__display-text {
+ color: var(--text-less-prominent);
+ }
+ }
+ &--error {
+ @media (max-width: 992px) {
+ margin-bottom: 5rem;
+ }
+
+ label {
+ color: var(--brand-red-coral) !important;
+ }
+
+ & ::placeholder {
+ color: var(--text-loss-danger) !important;
+ opacity: 1 !important;
+ }
+ }
+ &__container {
+ display: flex;
+ align-items: center;
+ border-radius: $BORDER_RADIUS;
+ border: 1px solid var(--border-normal);
+ padding: 0.5rem 1.2rem;
+ height: 4rem;
+
+ &:hover:not(.dc-input--disabled) {
+ border-color: var(--general-disabled);
+ }
+ &:focus-within {
+ border-color: var(--brand-secondary);
+
+ &:hover {
+ border-color: var(--brand-secondary);
+ }
+ }
+ &--error {
+ border-color: var(--brand-red-coral) !important;
+ }
+ &--disabled {
+ border-color: var(--general-disabled);
+ }
+ }
+ &__field {
+ background: none;
+ color: var(--text-prominent);
+ font-size: var(--text-size-xs);
+ width: 100%;
+ height: 100%;
+ min-width: 0;
+
+ &::placeholder {
+ line-height: 1.5;
+ opacity: 0;
+ transition: opacity 0.25s;
+
+ /* To vertically align placeholder in Firefox */
+ @-moz-document url-prefix('') {
+ line-height: 1.25;
+ }
+ }
+ &--placeholder-visible::placeholder {
+ opacity: 0.4;
+ }
+ /* Not empty fields */
+ &:focus,
+ &:not(:focus):not([value='']) {
+ outline: none;
+
+ & ~ label {
+ transform: translate(0, -1.8rem) scale(0.75);
+ padding: 0 4px;
+ }
+ }
+ &:disabled {
+ -webkit-text-fill-color: var(--text-less-prominent); // iOS
+ opacity: 1; // iOS
+ color: var(--text-less-prominent);
+
+ & ~ label {
+ color: var(--text-less-prominent) !important;
+ }
+ & ~ svg {
+ .color1-fill {
+ fill: var(--text-less-prominent);
+ }
+ }
+ // TODO: Ugly safari override hack, find better way to override shadow dom generated by Safari
+ /* stylelint-disable */
+ @media not all and (min-resolution: 0.001dpcm) {
+ @supports (-webkit-appearance: none) {
+ color: var(--text-prominent);
+ }
+ }
+ /* stylelint-enable */
+ }
+ &:focus {
+ outline: none;
+
+ & ~ label {
+ color: var(--brand-secondary);
+ }
+ &::placeholder {
+ opacity: 0.4;
+ }
+ }
+ &:not(.dc-input--no-placeholder):not(:focus):not([value='']) {
+ & ~ label {
+ color: var(--text-general);
+ }
+ }
+ &[type='number']::-webkit-inner-spin-button,
+ &[type='number']::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+ &[type='number'] {
+ -moz-appearance: textfield;
+ }
+ &[type='textarea'] {
+ height: 9.6rem;
+ border: none;
+ resize: none;
+
+ ~ .dc-field--error {
+ top: 10rem;
+ }
+ }
+ /* To get rid of ugly chrome autofill shadow dom-applied background color */
+ &:-webkit-autofill {
+ border-radius: $BORDER_RADIUS;
+ -webkit-box-shadow: 0 0 0 30px var(--fill-normal) inset !important;
+ -webkit-text-fill-color: var(--text-prominent) !important;
+
+ &:hover,
+ &:focus,
+ &:active {
+ -webkit-box-shadow: 0 0 0 30px var(--fill-normal) inset !important;
+ }
+ }
+ // hide default eye icon in Edge browser
+ &::-ms-reveal {
+ display: none;
+ }
+ }
+ &__textarea {
+ &:not(.dc-input--no-placeholder):not(:focus):empty {
+ & ~ label {
+ transform: none;
+ color: var(--text-less-prominent);
+ padding: 0 4px;
+ }
+ }
+ }
+ &__leading-icon {
+ margin-left: 1rem;
+ top: 1rem;
+ position: absolute;
+ left: 0;
+ pointer-events: none;
+ cursor: text;
+ font-size: var(--text-size-xs);
+
+ &.symbols {
+ top: 0.9rem;
+
+ + .dc-input__field {
+ // default padding for three letter currency symbols
+ padding-left: calc(1.6rem + 2.4rem);
+ }
+ &--usdc + .dc-input__field,
+ &--ust + .dc-input__field {
+ padding-left: calc(1.6rem + 3.2rem);
+ }
+ }
+ }
+ &__trailing-icon {
+ right: 0;
+ font-size: var(--text-size-xs);
+
+ &.symbols {
+ top: 0.9rem;
+
+ + .dc-input__field {
+ // default padding for three letter currency symbols
+ padding-right: calc(1.6rem + 2.4rem);
+ }
+ &--usd {
+ top: 1rem;
+ right: 1.1rem;
+ position: absolute;
+ }
+ &--usdc + .dc-input__field,
+ &--ust + .dc-input__field {
+ padding-right: calc(1.6rem + 3.2rem);
+ }
+ }
+ }
+ &__label {
+ white-space: nowrap;
+ color: var(--text-less-prominent);
+ font-size: var(--text-size-xs);
+ background-color: var(--general-main-1);
+ position: absolute;
+ pointer-events: none;
+ left: 1.1rem;
+ top: 1.1rem;
+ transition: 0.25s ease all;
+ transform-origin: top left;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-width: calc(100% - 1.4rem);
+ }
+ &:not(&--no-placeholder) {
+ $parent: &;
+
+ #{$parent}__label {
+ color: var(--text-general);
+ transition: 0.25s ease all;
+ transform: translateZ(0);
+ }
+ }
+ &__hint {
+ margin: 0.1rem 0 -1.9rem 1.3rem;
+
+ &--relative {
+ margin-left: 1.2rem;
+ position: relative;
+ top: unset;
+ }
+ }
+ &__counter {
+ color: var(--text-less-prominent);
+ font-size: 1.2rem;
+ margin-left: 1.2rem;
+ }
+ &--no-placeholder {
+ label {
+ transform: translate(0, -1.8rem) scale(0.75);
+ color: var(--text-prominent);
+ padding: 0 4px;
+ background-color: var(--fill-normal);
+ }
+ }
+ &__footer {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: -1.6rem;
+
+ > :not(.dc-input__counter) {
+ margin-right: 1.2rem;
+ }
+
+ > .dc-input__counter {
+ margin-left: auto;
+ }
+ }
+ &__wrapper {
+ margin-bottom: 1.6rem;
+ }
+ &__input-group {
+ border-right-style: none;
+ border-radius: 4px 0 0 4px;
+ }
+}
diff --git a/src/components/shared_ui/input/input.tsx b/src/components/shared_ui/input/input.tsx
new file mode 100644
index 00000000..71b2d5f0
--- /dev/null
+++ b/src/components/shared_ui/input/input.tsx
@@ -0,0 +1,240 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Text } from '@deriv-com/ui';
+
+import Field from '../field';
+
+export type TInputProps = {
+ autoComplete?: string;
+ bottom_label?: string;
+ className?: string;
+ classNameError?: string;
+ classNameHint?: string;
+ classNameWarn?: string;
+ data_testId?: string;
+ disabled?: boolean;
+ error?: string;
+ field_className?: string;
+ has_character_counter?: boolean;
+ hint?: React.ReactNode;
+ id?: string;
+ initial_character_count?: number;
+ inputMode?: React.HTMLAttributes['inputMode'];
+ input_id?: string;
+ is_relative_hint?: boolean;
+ label_className?: string;
+ label?: string;
+ leading_icon?: React.ReactElement;
+ max_characters?: number;
+ maxLength?: number;
+ name?: string;
+ onBlur?: React.FocusEventHandler;
+ onChange?: React.ChangeEventHandler;
+ onMouseDown?: React.MouseEventHandler;
+ onMouseUp?: React.MouseEventHandler;
+ onFocus?: React.FocusEventHandler;
+ onPaste?: React.ClipboardEventHandler;
+ onKeyUp?: React.FormEventHandler;
+ onKeyDown?: React.FormEventHandler;
+ onInput?: React.FormEventHandler;
+ onClick?: React.MouseEventHandler;
+ onMouseEnter?: React.MouseEventHandler;
+ onMouseLeave?: React.MouseEventHandler;
+ placeholder?: string;
+ ref?: React.RefObject<
+ React.ComponentType extends 'textarea'
+ ? HTMLTextAreaElement
+ : React.ComponentType extends 'input'
+ ? HTMLInputElement
+ : never
+ >;
+ required?: boolean;
+ trailing_icon?: React.ReactElement | null;
+ type?: string;
+ value?: string | number;
+ warn?: string;
+ readOnly?: boolean;
+ is_autocomplete_disabled?: string;
+ is_hj_whitelisted?: string;
+} & Omit, 'ref'>;
+
+type TInputWrapper = {
+ has_footer: boolean;
+};
+
+const InputWrapper = ({ children, has_footer }: React.PropsWithChildren) =>
+ has_footer ? {children}
: {children} ;
+
+const Input = React.forwardRef(
+ (
+ {
+ bottom_label,
+ className,
+ classNameError,
+ classNameHint,
+ classNameWarn,
+ disabled = false,
+ error,
+ field_className,
+ has_character_counter,
+ hint,
+ initial_character_count,
+ input_id,
+ is_relative_hint,
+ label_className,
+ label,
+ leading_icon,
+ max_characters,
+ trailing_icon,
+ warn,
+ data_testId,
+ maxLength,
+ placeholder,
+ ...props
+ },
+ ref?
+ ) => {
+ const [counter, setCounter] = React.useState(0);
+
+ React.useEffect(() => {
+ if (initial_character_count || initial_character_count === 0) {
+ setCounter(initial_character_count);
+ }
+ }, [initial_character_count]);
+
+ const changeHandler: React.ChangeEventHandler = e => {
+ let input_value = e.target.value;
+ if (max_characters && input_value.length >= max_characters) {
+ input_value = input_value.slice(0, max_characters);
+ }
+ setCounter(input_value.length);
+ e.target.value = input_value;
+ props.onChange?.(e);
+ };
+
+ const has_footer = !!has_character_counter || (!!hint && !!is_relative_hint);
+ const field_placeholder = label ? '' : placeholder;
+
+ return (
+
+
+
+ {leading_icon &&
+ React.cloneElement(leading_icon, {
+ className: classNames('dc-input__leading-icon', leading_icon.props.className),
+ })}
+ {props.type === 'textarea' ? (
+
+
+ {!has_footer && (
+
+ {error && }
+ {warn && }
+ {!error && hint && !is_relative_hint && (
+
+
+ {hint}
+
+
+ )}
+
+ )}
+
+
+ {has_footer && (
+ // Added like below for backward compatibility.
+ // TODO: refactor existing usages to use "relative" hints
+ // i.e. get rid of absolute hints, errors, counters.
+
+ {error &&
}
+ {warn &&
}
+ {!error && hint && (
+
+
+ {hint}
+
+
+ )}
+ {has_character_counter && (
+
+
+ {counter}
+ {max_characters ? `/${max_characters}` : ''}
+
+
+ )}
+
+ )}
+ {bottom_label && !error && (
+
+
+ {bottom_label}
+
+
+ )}
+
+ );
+ }
+);
+
+Input.displayName = 'Input';
+
+export default Input;
diff --git a/src/components/shared_ui/mobile-dialog/index.ts b/src/components/shared_ui/mobile-dialog/index.ts
new file mode 100644
index 00000000..eae8e8e3
--- /dev/null
+++ b/src/components/shared_ui/mobile-dialog/index.ts
@@ -0,0 +1,5 @@
+import MobileDialog from './mobile-dialog';
+
+import './mobile-dialog.scss';
+
+export default MobileDialog;
diff --git a/src/components/shared_ui/mobile-dialog/mobile-dialog.scss b/src/components/shared_ui/mobile-dialog/mobile-dialog.scss
new file mode 100644
index 00000000..791a97dc
--- /dev/null
+++ b/src/components/shared_ui/mobile-dialog/mobile-dialog.scss
@@ -0,0 +1,138 @@
+/** @define dc-mobile-dialog; weak */
+.dc-mobile-dialog {
+ box-sizing: border-box;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ z-index: 999;
+ overflow: auto;
+ transition: opacity 0.2s;
+ opacity: 0;
+ pointer-events: none;
+ background: var(--fill-normal);
+ // transform here would break fixed header
+
+ &--enter,
+ &--exit {
+ opacity: 0;
+ pointer-events: none;
+
+ & .dc-mobile-dialog__content {
+ transform: scale(0);
+ }
+ & .dc-mobile-dialog__header {
+ transform: translateY(-100%);
+ }
+ }
+ &--enter-done {
+ opacity: 1;
+ pointer-events: auto;
+
+ & .dc-mobile-dialog__content {
+ transform: scale(1);
+ }
+ & .dc-mobile-dialog__header {
+ transform: translateY(0);
+ }
+ }
+ &__content {
+ margin-top: 0.4rem;
+ box-sizing: border-box;
+ position: relative;
+ padding-top: 3.6rem;
+ z-index: 1;
+ background: var(--fill-normal);
+ transition: all 0.2s ease-out;
+
+ &--is-full-height {
+ height: calc(100% - 0.4rem);
+ }
+ > div {
+ height: 100%;
+ }
+ }
+ &__header {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ position: fixed;
+ top: 0;
+ padding: 1rem;
+ height: $MOBILE_HEADER_HEIGHT;
+ z-index: 4;
+ background: var(--fill-normal);
+ transition: all 0.2s ease-out;
+ transition-delay: 0.2s;
+ border-bottom: 1px solid var(--border-disabled);
+ }
+ &__container {
+ &--has-scroll {
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+ &--has-info-banner {
+ .dc-mobile-dialog {
+ &__header {
+ position: relative;
+ top: unset;
+ z-index: unset;
+ background: unset;
+
+ &-wrapper {
+ position: sticky;
+ top: 0;
+ z-index: 4;
+ background: var(--fill-normal);
+ width: 100%;
+ // transparent border ensures children's margins contribute to height:
+ border-bottom: 0.01rem solid transparent;
+
+ .inline-message__information {
+ margin: 1.6rem 0.8rem -0.8rem;
+ }
+
+ .learn-more {
+ height: 5rem;
+ width: calc(100% - 1.6rem);
+ margin: 1.6rem 0.8rem 0.8rem;
+ padding: 1.6rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border: none;
+ background-color: var(--general-section-1);
+ cursor: pointer;
+ border-radius: $BORDER_RADIUS;
+ }
+ }
+ }
+ &__content {
+ margin-top: unset;
+ padding-top: unset;
+ }
+ }
+ }
+ }
+ &__footer {
+ position: fixed;
+ width: 100%;
+ bottom: 0px;
+ z-index: 4;
+ }
+ &__title {
+ padding: 1.2rem 1.2rem 1.2rem 0.4rem;
+ margin: 0;
+ }
+ &__close-btn-icon {
+ margin: 1.2rem 0.4rem;
+ width: 1.6rem;
+ height: 1.6rem;
+ /* postcss-bem-linter: ignore */
+ --fill-color1: var(--text-prominent) !important;
+ }
+}
diff --git a/src/components/shared_ui/mobile-dialog/mobile-dialog.tsx b/src/components/shared_ui/mobile-dialog/mobile-dialog.tsx
new file mode 100644
index 00000000..9828501a
--- /dev/null
+++ b/src/components/shared_ui/mobile-dialog/mobile-dialog.tsx
@@ -0,0 +1,175 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import Div100vhContainer from '../div100vh-container';
+import Text from '../text/text';
+import ThemedScrollbars from '../themed-scrollbars';
+
+type TMobileDialog = {
+ content_height_offset?: string;
+ footer?: React.ReactNode;
+ has_content_scroll?: boolean;
+ has_close_icon?: boolean;
+ has_full_height?: boolean;
+ header_classname?: string;
+ info_banner?: React.ReactNode;
+ onClose?: React.MouseEventHandler;
+ portal_element_id: string;
+ renderTitle?: () => string;
+ title?: React.ReactNode;
+ visible?: boolean;
+ wrapper_classname?: string;
+ learn_more_banner?: React.ReactNode;
+};
+
+const MobileDialog = (props: React.PropsWithChildren) => {
+ const {
+ children,
+ footer,
+ has_close_icon = true,
+ has_full_height,
+ header_classname,
+ info_banner,
+ portal_element_id,
+ renderTitle,
+ title,
+ visible,
+ wrapper_classname,
+ learn_more_banner,
+ } = props;
+
+ const footer_ref = React.useRef(null);
+ const [footer_height, setHeight] = React.useState(0);
+ React.useLayoutEffect(() => {
+ if (footer_ref.current && !footer_height) {
+ setHeight(footer_ref.current.offsetHeight);
+ }
+ }, [footer, footer_height]);
+
+ const portal_element = document.getElementById(portal_element_id);
+
+ const checkVisibility = () => {
+ if (props.visible) {
+ document.body.style.overflow = 'hidden';
+ if (portal_element) portal_element.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ if (portal_element) portal_element.style.overflow = 'unset';
+ }
+ };
+
+ const scrollToElement = (parent: HTMLInputElement, el: HTMLInputElement) => {
+ const viewport_offset = el.getBoundingClientRect();
+ const hidden = viewport_offset.top + el.clientHeight + 20 > window.innerHeight;
+ if (hidden) {
+ const new_el_top = (window.innerHeight - el.clientHeight) / 2;
+ parent.scrollTop += viewport_offset.top - new_el_top;
+ }
+ };
+
+ // sometimes input is covered by virtual keyboard on mobile chrome, uc browser
+ const handleClick = (e: React.MouseEvent) => {
+ const target = e.target as HTMLInputElement;
+ if (target.tagName !== 'A') e.stopPropagation();
+ if (target.tagName === 'INPUT' && target.type === 'number') {
+ const scrollToTarget = () => scrollToElement(e.currentTarget, target);
+ window.addEventListener('resize', scrollToTarget, false);
+
+ // remove listener, resize is not fired on iOS safari
+ window.setTimeout(() => {
+ window.removeEventListener('resize', scrollToTarget, false);
+ }, 2000);
+ }
+ };
+
+ checkVisibility();
+ if (!portal_element) return null;
+ return ReactDOM.createPortal(
+
+
+
+
+
+
+ {renderTitle ? renderTitle() : title}
+
+ {has_close_icon && (
+
+
+
+ )}
+
+ {info_banner}
+ {learn_more_banner}
+
+
+ {footer && (
+
+ {footer}
+
+ )}
+
+
+ ,
+ portal_element
+ );
+};
+
+export default MobileDialog;
diff --git a/src/components/shared_ui/mobile-full-page-modal/index.ts b/src/components/shared_ui/mobile-full-page-modal/index.ts
new file mode 100644
index 00000000..7f02de9d
--- /dev/null
+++ b/src/components/shared_ui/mobile-full-page-modal/index.ts
@@ -0,0 +1,5 @@
+import MobileFullPageModal from './mobile-full-page-modal';
+
+import './mobile-full-page-modal.scss';
+
+export default MobileFullPageModal;
diff --git a/src/components/shared_ui/mobile-full-page-modal/mobile-full-page-modal.scss b/src/components/shared_ui/mobile-full-page-modal/mobile-full-page-modal.scss
new file mode 100644
index 00000000..d711f865
--- /dev/null
+++ b/src/components/shared_ui/mobile-full-page-modal/mobile-full-page-modal.scss
@@ -0,0 +1,111 @@
+@import './../../shared/styles/constants.scss';
+
+.dc-mobile-full-page-modal {
+ height: calc(100% - #{$MOBILE_HEADER_HEIGHT} - #{$MOBILE_WRAPPER_HEADER_HEIGHT});
+ position: fixed;
+ z-index: 6;
+ width: 100%;
+ left: 0px;
+ top: calc(#{$MOBILE_HEADER_HEIGHT} + #{$MOBILE_WRAPPER_HEADER_HEIGHT});
+ background: var(--general-main-1);
+ max-height: 100%;
+ overflow-y: scroll;
+
+ &--flex {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ }
+
+ &--popup {
+ height: calc(100% - #{$MOBILE_HEADER_HEIGHT} - 1px);
+ top: calc(#{$MOBILE_HEADER_HEIGHT} - 1px);
+ }
+
+ &__header {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ padding: 1.6rem;
+ flex: 0;
+
+ &--border-bottom {
+ border-bottom: 2px solid var(--general-section-2);
+ }
+
+ &-return {
+ display: flex;
+ align-items: center;
+ margin-right: 1.6rem;
+ }
+ &-text {
+ display: flex;
+ flex-direction: row;
+ }
+ &-trailing-icon {
+ align-items: center;
+ display: flex;
+ margin-left: auto;
+ }
+ }
+
+ &__body {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ overflow-y: auto;
+ }
+
+ &__footer {
+ &-parent {
+ border-top: 1px solid var(--general-section-2);
+ padding: 0.8rem 1.6rem;
+ background-color: var(--general-main-1);
+ }
+
+ border-top: 2px solid var(--general-section-1);
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ margin-top: auto;
+ padding: 1.6rem;
+ }
+
+ form {
+ height: 100%;
+ }
+
+ & .dc {
+ &-page-overlay {
+ &__content {
+ position: fixed;
+ top: calc(#{$MOBILE_HEADER_HEIGHT} + #{$MOBILE_WRAPPER_HEADER_HEIGHT});
+ width: 100%;
+ height: 100%;
+ }
+ &__header {
+ position: fixed;
+ top: #{$MOBILE_HEADER_HEIGHT};
+ width: 100%;
+ height: #{$MOBILE_WRAPPER_HEADER_HEIGHT};
+ }
+ }
+ &-tabs {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+
+ &__list {
+ width: 100%;
+ height: 4rem;
+ }
+ &__item {
+ width: 50%;
+ }
+ &__content {
+ height: calc(100% - 40px);
+ z-index: 5;
+ }
+ }
+ }
+}
diff --git a/src/components/shared_ui/mobile-full-page-modal/mobile-full-page-modal.tsx b/src/components/shared_ui/mobile-full-page-modal/mobile-full-page-modal.tsx
new file mode 100644
index 00000000..920fe2b5
--- /dev/null
+++ b/src/components/shared_ui/mobile-full-page-modal/mobile-full-page-modal.tsx
@@ -0,0 +1,132 @@
+import * as React from 'react';
+import classNames from 'classnames';
+
+import { Text } from '@deriv-com/ui';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import Div100vhContainer from '../div100vh-container';
+import FadeWrapper from '../fade-wrapper';
+import PageOverlay from '../page-overlay/page-overlay';
+
+type TMobileFullPageModal = {
+ className?: string;
+ container_children?: React.ReactNode;
+ header?: string;
+ header_background_color?: string;
+ height_offset?: string;
+ is_flex?: boolean;
+ is_modal_open: boolean;
+ onClickClose?: (event: MouseEvent) => void;
+ pageHeaderReturnFn?: () => void;
+ renderPageFooterChildren?: () => React.ReactNode;
+ page_footer_className?: string;
+ page_header_className?: string;
+ page_header_text?: string;
+ renderPageHeaderElement?: JSX.Element;
+ renderPageHeaderTrailingIcon?: () => React.ReactNode;
+ should_header_stick_body?: boolean;
+ body_className?: string;
+ is_popup?: boolean;
+ page_footer_parent?: React.ReactNode;
+ renderPageHeader?: () => React.ReactNode;
+ page_footer_parent_className?: string;
+};
+
+const MobileFullPageModal = ({
+ body_className,
+ className,
+ should_header_stick_body,
+ header,
+ header_background_color,
+ height_offset = '0px',
+ is_flex,
+ is_popup,
+ is_modal_open,
+ onClickClose,
+ renderPageFooterChildren,
+ page_footer_className,
+ page_footer_parent,
+ page_footer_parent_className,
+ page_header_className,
+ page_header_text,
+ renderPageHeaderTrailingIcon,
+ pageHeaderReturnFn,
+ renderPageHeader,
+ renderPageHeaderElement,
+ // opt-in for backward compatibility.
+ children,
+ container_children,
+}: React.PropsWithChildren) => (
+
+
+
+ {(renderPageHeader || page_header_text || renderPageHeaderElement) && (
+
+ {pageHeaderReturnFn && (
+
+
+
+ )}
+ {renderPageHeader && renderPageHeader()}
+
+ {renderPageHeaderElement ?? (
+
+ {page_header_text}
+
+ )}
+
+ {renderPageHeaderTrailingIcon && (
+
+ {renderPageHeaderTrailingIcon()}
+
+ )}
+
+ )}
+ {children}
+ {renderPageFooterChildren && (
+
+ {page_footer_parent && (
+
+ {page_footer_parent}
+
+ )}
+
+ {renderPageFooterChildren()}
+
+
+ )}
+ {container_children}
+
+
+
+);
+
+export default MobileFullPageModal;
diff --git a/src/components/shared_ui/mobile-wrapper/index.ts b/src/components/shared_ui/mobile-wrapper/index.ts
new file mode 100644
index 00000000..30b6e174
--- /dev/null
+++ b/src/components/shared_ui/mobile-wrapper/index.ts
@@ -0,0 +1,5 @@
+import MobileWrapper from './mobile-wrapper';
+
+import './mobile-wrapper.scss';
+
+export default MobileWrapper;
diff --git a/src/components/shared_ui/mobile-wrapper/mobile-wrapper.scss b/src/components/shared_ui/mobile-wrapper/mobile-wrapper.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/shared_ui/mobile-wrapper/mobile-wrapper.tsx b/src/components/shared_ui/mobile-wrapper/mobile-wrapper.tsx
new file mode 100644
index 00000000..97a6328e
--- /dev/null
+++ b/src/components/shared_ui/mobile-wrapper/mobile-wrapper.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import { isMobile } from '@/components/shared/utils/screen';
+
+type TMobileWrapper = {
+ children: React.ReactNode;
+};
+
+const MobileWrapper = ({ children }: TMobileWrapper) => {
+ if (!isMobile()) return null;
+
+ return {children} ;
+};
+
+export default MobileWrapper;
diff --git a/src/components/shared_ui/money/index.ts b/src/components/shared_ui/money/index.ts
new file mode 100644
index 00000000..6e7c9052
--- /dev/null
+++ b/src/components/shared_ui/money/index.ts
@@ -0,0 +1,5 @@
+import Money from './money';
+
+import './money.scss';
+
+export default Money;
diff --git a/src/components/shared_ui/money/money.scss b/src/components/shared_ui/money/money.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/shared_ui/money/money.tsx b/src/components/shared_ui/money/money.tsx
new file mode 100644
index 00000000..da225633
--- /dev/null
+++ b/src/components/shared_ui/money/money.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+
+import { formatMoney, getCurrencyDisplayCode } from '@/components/shared';
+
+type TMoneyProps = {
+ amount: number | string;
+ className: string;
+ currency: string;
+ has_sign: boolean;
+ should_format: boolean;
+ show_currency: boolean; // if true, append currency symbol
+};
+
+const Money = ({
+ amount = 0,
+ className,
+ currency = 'USD',
+ has_sign,
+ should_format = true,
+ show_currency = false,
+}: Partial) => {
+ let sign = '';
+ if (Number(amount) && (Number(amount) < 0 || has_sign)) {
+ sign = Number(amount) > 0 ? '+' : '-';
+ }
+
+ // if it's formatted already then don't make any changes unless we should remove extra -/+ signs
+ const value = has_sign || should_format ? Math.abs(Number(amount)) : amount;
+ const final_amount = should_format ? formatMoney(currency, value, true, 0, 0) : value;
+
+ return (
+
+ {has_sign && sign}
+
+ {final_amount} {show_currency && getCurrencyDisplayCode(currency)}
+
+
+ );
+};
+
+export default React.memo(Money);
diff --git a/src/components/shared_ui/page-overlay/index.ts b/src/components/shared_ui/page-overlay/index.ts
new file mode 100644
index 00000000..d24400c7
--- /dev/null
+++ b/src/components/shared_ui/page-overlay/index.ts
@@ -0,0 +1,5 @@
+import PageOverlay from './page-overlay';
+
+import './page-overlay.scss';
+
+export default PageOverlay;
diff --git a/src/components/shared_ui/page-overlay/page-overlay.scss b/src/components/shared_ui/page-overlay/page-overlay.scss
new file mode 100644
index 00000000..4916f45f
--- /dev/null
+++ b/src/components/shared_ui/page-overlay/page-overlay.scss
@@ -0,0 +1,111 @@
+@import './../../shared/styles/constants.scss';
+@import './../../shared/styles/devices.scss';
+
+.dc-page-overlay {
+ &-portal {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ top: $HEADER_HEIGHT;
+ transition:
+ transform 0.25s cubic-bezier(0.25, 0.1, 0.1, 0.25),
+ opacity 0.25s cubic-bezier(0.25, 0.1, 0.1, 0.25);
+ background-color: var(--general-main-2);
+ opacity: 0;
+
+ @include mobile {
+ top: $MOBILE_HEADER_HEIGHT;
+ }
+ }
+ &--enter,
+ &--exit {
+ transform: translateY(50px);
+ opacity: 0;
+ pointer-events: none;
+ }
+ &--enter-done {
+ transform: translateY(0);
+ opacity: 1;
+ pointer-events: auto;
+ z-index: 9;
+ }
+ &__header {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ background-color: var(--general-main-1);
+ padding: 0.8rem 2.4rem;
+ border-bottom: 1px solid var(--general-section-1);
+
+ &-title {
+ flex: 1;
+ text-align: center;
+ font-size: var(--text-size-m);
+ color: var(--text-prominent);
+ font-weight: bold;
+ line-height: 1.5;
+
+ @include mobile {
+ font-size: 1.6rem;
+ }
+ }
+ &-close {
+ cursor: pointer;
+ height: 16px;
+ position: absolute;
+ right: 0;
+
+ & .dc-icon {
+ --fill-color1: var(--text-prominent);
+ }
+ @include mobile {
+ right: 1.2rem;
+ }
+ }
+ &-wrapper {
+ width: 100%;
+ display: flex;
+ align-items: center;
+
+ @include mobile {
+ height: 100%;
+ padding: 0;
+ }
+ }
+ @include mobile {
+ height: 4rem;
+ z-index: 3;
+ padding: 0;
+ margin: 0;
+ }
+ }
+ &__content {
+ padding: 2.4rem 0 0;
+ display: flex;
+ background-color: var(--general-main-1);
+
+ &-side-note {
+ min-width: 256px;
+ margin-left: 2.4rem;
+ }
+ @include mobile {
+ flex: 1;
+ padding: 0;
+ }
+ }
+ &__header-wrapper,
+ &__content {
+ max-width: 1232px;
+ margin: auto;
+ position: relative;
+
+ @include mobile {
+ width: 100%;
+ }
+ }
+ @include mobile {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+}
diff --git a/src/components/shared_ui/page-overlay/page-overlay.tsx b/src/components/shared_ui/page-overlay/page-overlay.tsx
new file mode 100644
index 00000000..5451144e
--- /dev/null
+++ b/src/components/shared_ui/page-overlay/page-overlay.tsx
@@ -0,0 +1,93 @@
+import React, { MouseEventHandler } from 'react';
+import ReactDOM from 'react-dom';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+type TPageOverlay = {
+ header?: React.ReactNode;
+ id?: string;
+ is_from_app?: boolean;
+ is_open?: boolean;
+ onClickClose?: (event: MouseEvent) => void;
+ portal_id?: string;
+ header_classname?: string;
+ has_return_icon?: boolean;
+ onReturn?: () => void;
+};
+
+const PageOverlay = ({
+ children,
+ header,
+ id,
+ is_from_app = false,
+ is_open,
+ onClickClose,
+ portal_id,
+ header_classname,
+ has_return_icon,
+ onReturn,
+}: React.PropsWithChildren) => {
+ const page_overlay_ref = React.useRef(null);
+
+ const el_page_overlay = (
+
+ {header && (
+
+
+
+ {has_return_icon && (
+
+ )}
+ {header}
+
+ {!is_from_app && (
+
) ||
+ window.history.back
+ }
+ >
+
+
+ )}
+
+
+ )}
+
{children}
+
+ );
+
+ if (portal_id) {
+ return ReactDOM.createPortal(
+
+ {el_page_overlay}
+ ,
+ document.getElementById(portal_id) as HTMLElement
+ );
+ }
+
+ return {el_page_overlay} ;
+};
+
+export default PageOverlay;
diff --git a/src/components/shared_ui/popover/index.ts b/src/components/shared_ui/popover/index.ts
new file mode 100644
index 00000000..b9fa3f77
--- /dev/null
+++ b/src/components/shared_ui/popover/index.ts
@@ -0,0 +1,5 @@
+import Popover from './popover';
+
+import './popover.scss';
+
+export default Popover;
diff --git a/src/components/shared_ui/popover/popover.scss b/src/components/shared_ui/popover/popover.scss
new file mode 100644
index 00000000..de5e48ba
--- /dev/null
+++ b/src/components/shared_ui/popover/popover.scss
@@ -0,0 +1,94 @@
+.dc-popover {
+ &__wrapper {
+ position: relative;
+ }
+ &__container {
+ position: absolute;
+ /*rtl:ignore*/
+ left: 0;
+ top: 0;
+ width: 280px;
+
+ &-relative {
+ position: relative;
+ }
+ }
+ &__target {
+ // &__icon .info {
+ // @extend .dc-icon--secondary;
+
+ // &:hover {
+ // @extend .dc-icon;
+ // }
+ // }
+ // &__icon--disabled .info {
+ // @extend .dc-icon--disabled;
+
+ // &:hover {
+ // @extend .dc-icon--disabled;
+ // }
+ // }
+ &__icon .counter {
+ color: var(--brand-red-coral);
+ font-size: 10px;
+ font-weight: bold;
+ line-height: 1.4em;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-65%);
+ }
+ }
+ &__bubble {
+ @include typeface(--small-center-normal-active, none);
+ cursor: help;
+ position: relative;
+ padding: 0.8rem;
+ max-width: 31.7rem;
+ border-radius: 4px;
+ display: flex;
+ flex-direction: row;
+ background: var(--general-active);
+ color: var(--text-prominent);
+ z-index: 1;
+
+ &__icon {
+ flex-grow: 1;
+ margin-right: 0.7em;
+ padding-top: 3px;
+ }
+ &__text {
+ flex-grow: 2;
+ direction: ltr;
+ }
+ &--error {
+ direction: ltr;
+ background-color: var(--status-danger);
+ }
+ }
+ &__trade-params {
+ @include mobile {
+ max-width: calc(100vw - 6.7rem);
+ }
+ }
+}
+
+.mobile-widget__item-popover,
+.accu-info-display__popover {
+ max-width: 28rem;
+}
+.accu-info-display__popover {
+ @include mobile {
+ max-width: 33rem;
+ margin-right: 1rem;
+ }
+}
+
+.react-tiny-popover-container {
+ &--disabled-pointer-event {
+ pointer-events: none;
+ }
+}
+
+.react-tiny-popover-cursor-option {
+ cursor: pointer;
+}
diff --git a/src/components/shared_ui/popover/popover.tsx b/src/components/shared_ui/popover/popover.tsx
new file mode 100644
index 00000000..b8fda5b9
--- /dev/null
+++ b/src/components/shared_ui/popover/popover.tsx
@@ -0,0 +1,236 @@
+import React, { RefObject } from 'react';
+import { ArrowContainer, Popover as TinyPopover } from 'react-tiny-popover';
+import classNames from 'classnames';
+
+import { Text, useDevice } from '@deriv-com/ui';
+
+import { useHover, useHoverCallback } from '@/hooks/use-hover';
+import { Icon } from '@/utils/tmp/dummy';
+
+import { TPopoverProps } from '../types';
+
+const Popover = ({
+ alignment,
+ children,
+ className,
+ classNameBubble,
+ classNameTarget,
+ classNameTargetIcon,
+ counter,
+ disable_message_icon,
+ disable_target_icon,
+ has_error,
+ icon,
+ id,
+ is_open,
+ is_bubble_hover_enabled,
+ margin = 0,
+ message,
+ onBubbleClose,
+ onBubbleOpen,
+ onClick = () => undefined,
+ relative_render = false,
+ should_disable_pointer_events = false,
+ should_show_cursor,
+ zIndex = '1',
+ data_testid,
+ arrow_styles,
+}: React.PropsWithChildren) => {
+ const ref = React.useRef();
+ const [popover_ref, setPopoverRef] = React.useState(undefined);
+ const [is_bubble_visible, setIsBubbleVisible] = React.useState(false);
+ const { is_mobile } = useDevice();
+ const [hover_ref, is_hovered] = useHover(null, true);
+ const [bubble_hover_ref, is_bubble_hovered] = useHoverCallback();
+ const should_toggle_on_target_tap = React.useMemo(() => is_mobile && is_open === undefined, [is_mobile, is_open]);
+
+ React.useEffect(() => {
+ if (ref.current) {
+ setPopoverRef(ref.current);
+ }
+ }, [has_error]);
+ React.useEffect(() => {
+ if (!is_hovered && should_toggle_on_target_tap) {
+ setIsBubbleVisible(false);
+ }
+ }, [is_hovered, should_toggle_on_target_tap]);
+
+ const onMouseEnter = () => {
+ if (onBubbleOpen) onBubbleOpen();
+ };
+
+ const onMouseLeave = () => {
+ if (onBubbleClose) onBubbleClose();
+ };
+
+ const icon_class_name = classNames(classNameTargetIcon, icon);
+ const is_open_on_focus = is_hovered && message && (!should_toggle_on_target_tap || is_bubble_visible);
+
+ return (
+ }
+ className={classNames({ 'dc-popover__wrapper': relative_render })}
+ onClick={(e: React.MouseEvent
) => {
+ onClick(e);
+ if (should_toggle_on_target_tap) setIsBubbleVisible(!is_bubble_visible);
+ }}
+ data-testid='dt_popover_wrapper'
+ >
+ {relative_render && (
+
+
}
+ className='dc-popover__container-relative'
+ data-testid='dt_popover_relative_container'
+ />
+
+ )}
+ {(popover_ref || !relative_render) && (
+
{
+ const screen_width = document.body.clientWidth;
+ const total_width = childRect.right + (popoverRect.width - childRect.width / 2);
+ let top_offset = 0;
+ let left_offset = 0;
+
+ switch (alignment) {
+ case 'left': {
+ left_offset =
+ Math.abs(
+ (popoverRect.height > popoverRect.width
+ ? nudgedLeft
+ : popoverRect.width) + margin
+ ) * -1;
+ top_offset =
+ childRect.height > popoverRect.height
+ ? (childRect.height - popoverRect.height) / 2
+ : ((popoverRect.height - childRect.height) / 2) * -1;
+ break;
+ }
+ case 'right': {
+ left_offset = popoverRect.width + margin;
+ top_offset =
+ childRect.height > popoverRect.height
+ ? (childRect.height - popoverRect.height) / 2
+ : ((popoverRect.height - childRect.height) / 2) * -1;
+ break;
+ }
+ case 'top': {
+ left_offset =
+ total_width > screen_width
+ ? Math.abs(total_width - screen_width) * -1
+ : 0;
+ top_offset = Math.abs(popoverRect.height + margin) * -1;
+ break;
+ }
+ case 'bottom': {
+ left_offset =
+ total_width > screen_width
+ ? Math.abs(total_width - screen_width) * -1
+ : 0;
+ top_offset = childRect.height + margin;
+ break;
+ }
+ default:
+ break;
+ }
+ return {
+ top: top_offset,
+ left: left_offset,
+ };
+ },
+ }
+ : { containerStyle: { zIndex } })}
+ content={({ position, childRect, popoverRect }) => {
+ return (
+
+ void}
+ >
+ {!disable_message_icon && icon === 'info' && (
+
+
+
+ )}
+ {(has_error && (
+
+ {message}
+
+ )) || (
+
+ {message}
+
+ )}
+
+
+ );
+ }}
+ >
+
+
+ {!disable_target_icon && (
+
+ {icon === 'info' && }
+ {icon === 'question' && }
+ {icon === 'dot' && }
+ {icon === 'counter' && {counter} }
+
+ )}
+
+ {children}
+
+
+
+ )}
+
+ );
+};
+
+export default Popover;
diff --git a/src/components/shared_ui/progress-bar-tracker/index.ts b/src/components/shared_ui/progress-bar-tracker/index.ts
new file mode 100644
index 00000000..e914f6a4
--- /dev/null
+++ b/src/components/shared_ui/progress-bar-tracker/index.ts
@@ -0,0 +1,5 @@
+import ProgressBarTracker from './progress-bar-tracker';
+
+import './progress-bar-tracker.scss';
+
+export default ProgressBarTracker;
diff --git a/src/components/shared_ui/progress-bar-tracker/progress-bar-tracker.scss b/src/components/shared_ui/progress-bar-tracker/progress-bar-tracker.scss
new file mode 100644
index 00000000..63a08088
--- /dev/null
+++ b/src/components/shared_ui/progress-bar-tracker/progress-bar-tracker.scss
@@ -0,0 +1,24 @@
+.dc-progress-bar-tracker {
+ display: flex;
+ justify-content: center;
+ cursor: pointer;
+
+ &-rectangle {
+ width: 2.5rem;
+ height: 0.8rem;
+ background-color: $color-red;
+ border-radius: 1rem;
+ }
+
+ &-circle {
+ width: 0.8rem;
+ height: 0.8rem;
+ margin: 0 0.4rem;
+ border-radius: 50%;
+ background-color: var(--fill-normal-1);
+ }
+
+ &-transition {
+ transition: all 0.24s linear;
+ }
+}
diff --git a/src/components/shared_ui/progress-bar-tracker/progress-bar-tracker.tsx b/src/components/shared_ui/progress-bar-tracker/progress-bar-tracker.tsx
new file mode 100644
index 00000000..42cdd2fd
--- /dev/null
+++ b/src/components/shared_ui/progress-bar-tracker/progress-bar-tracker.tsx
@@ -0,0 +1,34 @@
+import classNames from 'classnames';
+
+type TProgressBarTracker = {
+ step: number;
+ steps_list: Array;
+ onStepChange: (step_num: number) => void;
+ is_transition?: boolean;
+};
+
+const ProgressBarTracker = ({ step, steps_list, is_transition = false, onStepChange }: TProgressBarTracker) => (
+
+ {steps_list.map((step_item, index) => {
+ const active = step === index + 1;
+
+ const handleClick = () => {
+ onStepChange(index + 1);
+ };
+
+ return (
+
+ );
+ })}
+
+);
+
+export default ProgressBarTracker;
diff --git a/src/components/shared_ui/progress-slider-mobile/index.ts b/src/components/shared_ui/progress-slider-mobile/index.ts
new file mode 100644
index 00000000..84654fae
--- /dev/null
+++ b/src/components/shared_ui/progress-slider-mobile/index.ts
@@ -0,0 +1,5 @@
+import ProgressSliderMobile from './progress-slider-mobile';
+
+import './progress-slider-mobile.scss';
+
+export default ProgressSliderMobile;
diff --git a/src/components/shared_ui/progress-slider-mobile/progress-slider-mobile.scss b/src/components/shared_ui/progress-slider-mobile/progress-slider-mobile.scss
new file mode 100644
index 00000000..1c462126
--- /dev/null
+++ b/src/components/shared_ui/progress-slider-mobile/progress-slider-mobile.scss
@@ -0,0 +1,70 @@
+.dc-progress-slider-mobile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &__timer {
+ margin: 0.2rem auto;
+ }
+ &__infinite-loader {
+ position: relative;
+ height: 4px;
+ display: block;
+ width: 100%;
+ background-color: var(--state-hover);
+ border-radius: 2px;
+ background-clip: padding-box;
+ margin: 0.5rem 0 1rem;
+ overflow: hidden;
+
+ &--indeterminate {
+ background-color: var(--state-active);
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ will-change: left, right;
+ }
+ &:before {
+ animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
+ }
+ &:after {
+ animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
+ animation-delay: 1.15s;
+ }
+ }
+ @keyframes indeterminate {
+ 0% {
+ left: -35%;
+ right: 100%;
+ }
+ 60% {
+ left: 100%;
+ right: -90%;
+ }
+ 100% {
+ left: 100%;
+ right: -90%;
+ }
+ }
+ @keyframes indeterminate-short {
+ 0% {
+ left: -200%;
+ right: 100%;
+ }
+ 60% {
+ left: 107%;
+ right: -8%;
+ }
+ 100% {
+ left: 107%;
+ right: -8%;
+ }
+ }
+ }
+}
diff --git a/src/components/shared_ui/progress-slider-mobile/progress-slider-mobile.tsx b/src/components/shared_ui/progress-slider-mobile/progress-slider-mobile.tsx
new file mode 100644
index 00000000..e61c4ed8
--- /dev/null
+++ b/src/components/shared_ui/progress-slider-mobile/progress-slider-mobile.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { getTimePercentage } from '@/components/shared';
+
+import CircularProgress from '../circular-progress';
+import RemainingTime from '../remaining-time';
+import Text from '../text';
+import { TGetCardLables } from '../types';
+
+import ProgressTicksMobile from './progress-ticks-mobile';
+
+type TProgressSliderMobileProps = {
+ className?: string;
+ current_tick?: number | null;
+ expiry_time?: number;
+ is_loading?: boolean;
+ server_time: moment.Moment;
+ start_time?: number;
+ ticks_count?: number;
+ getCardLabels: TGetCardLables;
+};
+
+const ProgressSliderMobile = ({
+ className,
+ current_tick,
+ getCardLabels,
+ is_loading,
+ start_time,
+ expiry_time,
+ server_time,
+ ticks_count,
+}: TProgressSliderMobileProps) => {
+ const percentage = getTimePercentage(server_time, Number(start_time), Number(expiry_time));
+ return (
+
+ {ticks_count ? (
+
+ ) : (
+
+
+
+
+ {is_loading || percentage < 1 ? (
+ // TODO: Change this behavior in mobile
+
+ ) : (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default ProgressSliderMobile;
diff --git a/src/components/shared_ui/progress-slider-mobile/progress-ticks-mobile.tsx b/src/components/shared_ui/progress-slider-mobile/progress-ticks-mobile.tsx
new file mode 100644
index 00000000..c8fde697
--- /dev/null
+++ b/src/components/shared_ui/progress-slider-mobile/progress-ticks-mobile.tsx
@@ -0,0 +1,20 @@
+import Text from '../text';
+import TickProgress from '../tick-progress';
+import { TGetCardLables } from '../types';
+
+type TProgressTicksMobileProps = {
+ current_tick?: number | null;
+ ticks_count: number;
+ getCardLabels: TGetCardLables;
+};
+
+const ProgressTicksMobile = ({ current_tick, getCardLabels, ticks_count }: TProgressTicksMobileProps) => (
+
+
+ {getCardLabels().TICK} {current_tick}
+
+ 5 ? 2 : 1} size={ticks_count} value={current_tick} />
+
+);
+
+export default ProgressTicksMobile;
diff --git a/src/components/shared_ui/progress-slider/index.ts b/src/components/shared_ui/progress-slider/index.ts
new file mode 100644
index 00000000..e849406b
--- /dev/null
+++ b/src/components/shared_ui/progress-slider/index.ts
@@ -0,0 +1,5 @@
+import ProgressSlider from './progress-slider';
+
+import './progress-slider.scss';
+
+export default ProgressSlider;
diff --git a/src/components/shared_ui/progress-slider/progress-slider.scss b/src/components/shared_ui/progress-slider/progress-slider.scss
new file mode 100644
index 00000000..55222391
--- /dev/null
+++ b/src/components/shared_ui/progress-slider/progress-slider.scss
@@ -0,0 +1,163 @@
+/** @define dc-progress-slider */
+// Progress Slider
+.dc-progress-slider {
+ position: relative;
+ width: 100%;
+ padding: unset;
+ box-sizing: border-box;
+ margin: 8px 0;
+ border-bottom: 1px solid var(--general-section-1);
+
+ &--completed {
+ border-bottom: 1px solid var(--general-section-6);
+ margin: 0.4rem 0 0.8rem;
+ }
+ &__track {
+ background: var(--text-disabled);
+ position: relative;
+ margin: 2px 0 8px;
+ height: 6px;
+ width: 100%;
+ border-radius: #{$BORDER_RADIUS * 2};
+ }
+ &__ticks {
+ position: relative;
+
+ &-wrapper {
+ position: relative;
+ margin: 2px 0 8px;
+ height: 6px;
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ }
+ &-step {
+ height: 6px;
+ width: 100%;
+ margin: 0 2px;
+ position: relative;
+ background: var(--state-hover);
+
+ &:before {
+ position: absolute;
+ content: '';
+ transition: transform 0.25s ease-in;
+ transform: scale3d(0, 1, 1);
+ transform-origin: left;
+ left: 0;
+ top: 0;
+ height: 6px;
+ width: 100%;
+ background-color: var(--brand-secondary);
+ }
+ &--marked:before {
+ transform: scale3d(1, 1, 1);
+ }
+ &:first-child {
+ margin-left: 0;
+ }
+ &:last-child {
+ margin-right: 0;
+ }
+ &:first-child,
+ &:first-child:before {
+ border-top-left-radius: $BORDER_RADIUS;
+ border-bottom-left-radius: $BORDER_RADIUS;
+ }
+ &:last-child,
+ &:last-child:before {
+ border-top-right-radius: $BORDER_RADIUS;
+ border-bottom-right-radius: $BORDER_RADIUS;
+ }
+ }
+ &-caption {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+ &__line {
+ background: var(--state-hover);
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ border-radius: #{$BORDER_RADIUS * 2};
+ pointer-events: none;
+ transition: width 0.3s;
+
+ &--ticks {
+ height: 2px;
+ }
+ &--green {
+ background: var(--status-success) !important;
+ }
+ &--yellow {
+ background: var(--status-warning) !important;
+ }
+ &--red {
+ background: var(--status-danger) !important;
+ }
+ }
+ &__infinite-loader {
+ position: relative;
+ height: 4px;
+ display: block;
+ width: 100%;
+ background-color: var(--state-hover);
+ border-radius: 2px;
+ background-clip: padding-box;
+ margin: 0.5rem 0 1rem;
+ overflow: hidden;
+
+ &--indeterminate {
+ background-color: var(--state-active);
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ background-color: inherit;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ will-change: left, right;
+ }
+ &:before {
+ animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
+ }
+ &:after {
+ animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
+ animation-delay: 1.15s;
+ }
+ }
+ @keyframes indeterminate {
+ 0% {
+ left: -35%;
+ right: 100%;
+ }
+ 60% {
+ left: 100%;
+ right: -90%;
+ }
+ 100% {
+ left: 100%;
+ right: -90%;
+ }
+ }
+ @keyframes indeterminate-short {
+ 0% {
+ left: -200%;
+ right: 100%;
+ }
+ 60% {
+ left: 107%;
+ right: -8%;
+ }
+ 100% {
+ left: 107%;
+ right: -8%;
+ }
+ }
+ }
+}
diff --git a/src/components/shared_ui/progress-slider/progress-slider.tsx b/src/components/shared_ui/progress-slider/progress-slider.tsx
new file mode 100644
index 00000000..aefe0f18
--- /dev/null
+++ b/src/components/shared_ui/progress-slider/progress-slider.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import classNames from 'classnames';
+import moment from 'moment';
+
+import { getTimePercentage } from '@/components/shared';
+
+import RemainingTime from '../remaining-time';
+import Text from '../text';
+import { TGetCardLables } from '../types';
+
+import ProgressTicks from './progress-ticks';
+
+type TProgressSliderProps = {
+ className?: string;
+ current_tick: number | null;
+ expiry_time?: number;
+ getCardLabels: TGetCardLables;
+ is_loading: boolean;
+ server_time: moment.Moment;
+ start_time?: number;
+ ticks_count?: number;
+};
+
+const ProgressSlider = ({
+ className,
+ current_tick,
+ expiry_time,
+ getCardLabels,
+ is_loading,
+ server_time,
+ start_time,
+ ticks_count,
+}: TProgressSliderProps) => {
+ const percentage = getTimePercentage(server_time, Number(start_time), Number(expiry_time));
+ return (
+
+ {ticks_count ? (
+
+ ) : (
+
+
+
+
+ {is_loading || percentage < 1 ? (
+
+ ) : (
+ /* Calculate line width based on percentage of time left */
+
+
= 50,
+ 'dc-progress-slider__line--yellow': percentage < 50 && percentage >= 20,
+ 'dc-progress-slider__line--red': percentage < 20,
+ })}
+ style={{ width: `${percentage}%` }}
+ />
+
+ )}
+
+ )}
+
+ );
+};
+// Keypress events do not trigger on Safari due to the way it handles input type='range' elements, using focus on the input element also doesn't work for Safari.
+
+export default ProgressSlider;
diff --git a/src/components/shared_ui/progress-slider/progress-ticks.tsx b/src/components/shared_ui/progress-slider/progress-ticks.tsx
new file mode 100644
index 00000000..86e8d250
--- /dev/null
+++ b/src/components/shared_ui/progress-slider/progress-ticks.tsx
@@ -0,0 +1,33 @@
+import classNames from 'classnames';
+
+import Text from '../text';
+import { TGetCardLables } from '../types';
+
+type TProgressTicksProps = {
+ current_tick: number | null;
+ getCardLabels: TGetCardLables;
+ ticks_count: number;
+};
+
+const ProgressTicks = ({ current_tick, getCardLabels, ticks_count }: TProgressTicksProps) => {
+ const arr_ticks = Array.from(Array(ticks_count).keys());
+ return (
+
+
+ {getCardLabels().TICK} {current_tick}
+
+
+ {arr_ticks.map(idx => (
+
+ ))}
+
+
+ );
+};
+
+export default ProgressTicks;
diff --git a/src/components/shared_ui/radio-group/index.ts b/src/components/shared_ui/radio-group/index.ts
new file mode 100644
index 00000000..bfd3d313
--- /dev/null
+++ b/src/components/shared_ui/radio-group/index.ts
@@ -0,0 +1,5 @@
+import RadioGroup from './radio-group';
+
+import './radio-group.scss';
+
+export default RadioGroup;
diff --git a/src/components/shared_ui/radio-group/radio-group.scss b/src/components/shared_ui/radio-group/radio-group.scss
new file mode 100644
index 00000000..54ce0a77
--- /dev/null
+++ b/src/components/shared_ui/radio-group/radio-group.scss
@@ -0,0 +1,48 @@
+.dc-radio-group {
+ display: flex;
+ margin-top: 16px;
+ flex-direction: row;
+ align-items: center;
+ &__input {
+ display: none;
+ }
+ &__item {
+ display: flex;
+ @include typeface(--paragraph-left-normal-prominent);
+
+ cursor: pointer;
+ color: var(--text-general);
+ }
+ &__item:not(:last-child) {
+ margin-right: 16px;
+ }
+ &__circle {
+ border: 2px solid var(--text-general);
+ border-radius: 50%;
+ box-shadow: 0 0 1px 0 var(--shadow-menu);
+ min-width: 16px;
+ height: 16px;
+ transition: all 0.3s ease-in-out;
+ margin-right: 8px;
+ align-self: center;
+
+ &--disabled {
+ border-color: var(--border-disabled);
+ }
+ &--selected {
+ border-width: 4px;
+ border-color: var(--brand-red-coral);
+ }
+ &--error {
+ border-color: var(--text-less-prominent);
+ }
+ }
+ &__label {
+ &--disabled {
+ color: var(--text-disabled);
+ }
+ &--error {
+ color: var(--text-loss-danger);
+ }
+ }
+}
diff --git a/src/components/shared_ui/radio-group/radio-group.tsx b/src/components/shared_ui/radio-group/radio-group.tsx
new file mode 100644
index 00000000..f6097ce5
--- /dev/null
+++ b/src/components/shared_ui/radio-group/radio-group.tsx
@@ -0,0 +1,107 @@
+import React, { ChangeEvent } from 'react';
+import classNames from 'classnames';
+
+import Text from '../text';
+
+type TItem = React.HTMLAttributes & {
+ id?: string;
+ value: string;
+ label: string;
+ disabled?: boolean;
+ hidden?: boolean;
+ has_error?: boolean;
+};
+type TItemWrapper = {
+ should_wrap_items?: boolean;
+};
+type TRadioGroup = {
+ className?: string;
+ name: string;
+ onToggle: (e: ChangeEvent) => void;
+ required?: boolean;
+ selected: string;
+} & TItemWrapper;
+
+const ItemWrapper = ({ children, should_wrap_items }: React.PropsWithChildren) => {
+ if (should_wrap_items) {
+ return {children}
;
+ }
+
+ return {children} ;
+};
+
+const RadioGroup = ({
+ className,
+ name,
+ onToggle,
+ required,
+ selected,
+ should_wrap_items,
+ children,
+}: React.PropsWithChildren) => {
+ const [selected_option, setSelectedOption] = React.useState(selected);
+
+ React.useEffect(() => {
+ setSelectedOption(selected);
+ }, [selected]);
+
+ const onChange = (e: ChangeEvent) => {
+ setSelectedOption(e.target.value);
+ onToggle(e);
+ };
+
+ return (
+
+ {Array.isArray(children) &&
+ children
+ .filter(item => !item.props.hidden)
+ .map(item => (
+
+
+
+
+
+ {item.props.label}
+
+
+
+ ))}
+
+ );
+};
+
+const Item = ({ children, hidden = false, ...props }: React.PropsWithChildren) => (
+
+ {children}
+
+);
+
+RadioGroup.Item = Item;
+
+export default RadioGroup;
diff --git a/src/components/shared_ui/radio-group/radio.tsx b/src/components/shared_ui/radio-group/radio.tsx
new file mode 100644
index 00000000..8d1283fa
--- /dev/null
+++ b/src/components/shared_ui/radio-group/radio.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import Text from '../text';
+
+type TRadio = {
+ className?: string;
+ classNameLabel?: string;
+ defaultChecked: boolean;
+ id: string;
+ onChange: (e: React.ChangeEvent) => void;
+};
+
+const Radio = ({
+ children,
+ className,
+ classNameLabel,
+ defaultChecked,
+ id,
+ onChange, // This needs to be here so it's not included in `otherProps`
+ ...otherProps
+}: React.PropsWithChildren) => {
+ const [checked, setChecked] = React.useState(defaultChecked);
+
+ /*
+ * We use useEffect here to tell the Radio component to update itself
+ * when it's no longer selected
+ * This is because we're handling the state of what's selected in RadioGroup with the defaultChecked prop
+ */
+ React.useEffect(() => {
+ setChecked(defaultChecked);
+ }, [defaultChecked]);
+
+ const onInputChange = (e: React.ChangeEvent) => {
+ setChecked(e.target.checked);
+ onChange(e);
+ };
+
+ return (
+
+
+
+
+ {children}
+
+
+ );
+};
+
+export default Radio;
diff --git a/src/components/shared_ui/remaining-time/index.ts b/src/components/shared_ui/remaining-time/index.ts
new file mode 100644
index 00000000..7e11567f
--- /dev/null
+++ b/src/components/shared_ui/remaining-time/index.ts
@@ -0,0 +1,3 @@
+import RemainingTime from './remaining-time';
+
+export default RemainingTime;
diff --git a/src/components/shared_ui/remaining-time/remaining-time.tsx b/src/components/shared_ui/remaining-time/remaining-time.tsx
new file mode 100644
index 00000000..ac5fb34a
--- /dev/null
+++ b/src/components/shared_ui/remaining-time/remaining-time.tsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import moment from 'moment';
+
+import { formatDuration, getDiffDuration } from '@/components/shared';
+
+import { TGetCardLables } from '../types';
+
+type TRemainingTimeProps = {
+ end_time?: number;
+ start_time: moment.Moment;
+ format?: string;
+ getCardLabels: TGetCardLables;
+};
+
+const RemainingTime = ({ end_time, format, getCardLabels, start_time }: TRemainingTimeProps) => {
+ if (!end_time || start_time.unix() > +end_time) {
+ return {''} ;
+ }
+
+ const { days, timestamp } = formatDuration(getDiffDuration(start_time.unix(), end_time), format);
+ let remaining_time = timestamp;
+ if (days > 0) {
+ remaining_time = `${days} ${days > 1 ? getCardLabels().DAYS : getCardLabels().DAY} ${timestamp}`;
+ }
+ const is_zeroes = /^00:00$/.test(remaining_time);
+
+ return {!is_zeroes && {remaining_time}
} ;
+};
+
+export default RemainingTime;
diff --git a/src/components/shared_ui/select-native/index.ts b/src/components/shared_ui/select-native/index.ts
new file mode 100644
index 00000000..be89cef6
--- /dev/null
+++ b/src/components/shared_ui/select-native/index.ts
@@ -0,0 +1,5 @@
+import SelectNative from './select-native';
+
+import './select-native.scss';
+
+export default SelectNative;
diff --git a/src/components/shared_ui/select-native/select-native.scss b/src/components/shared_ui/select-native/select-native.scss
new file mode 100644
index 00000000..b297aab9
--- /dev/null
+++ b/src/components/shared_ui/select-native/select-native.scss
@@ -0,0 +1,131 @@
+.dc-select-native {
+ width: 100%;
+ position: relative;
+
+ &--hide-selected-value {
+ margin-left: 10px;
+ width: 40px;
+
+ .dc-select-native__wrapper {
+ width: 40px;
+ }
+ }
+ &__container {
+ border: 1px solid var(--border-normal);
+ border-radius: $BORDER_RADIUS;
+ display: flex;
+ align-items: center;
+
+ &:hover:not(.dc-input--disabled) {
+ border-color: var(--border-hover);
+ }
+ &:focus-within {
+ border-color: var(--brand-secondary);
+
+ &:hover {
+ border-color: var(--brand-secondary);
+ }
+ }
+ &--error {
+ border-color: var(--brand-red-coral) !important;
+ }
+ &--disabled {
+ border-color: var(--general-disabled);
+ }
+ }
+ &__wrapper {
+ height: 38px;
+
+ .dc-input {
+ margin-bottom: 0px;
+ }
+ }
+ &__arrow {
+ position: absolute;
+ right: 1.3rem;
+ top: 1.3rem;
+ --fill-color1: var(--text-less-prominent);
+ }
+ &__display {
+ height: 38px;
+ width: 100%;
+ max-width: calc(100vw - 4rem);
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ padding-left: 1.2rem;
+
+ &-text {
+ color: var(--text-prominent);
+ font-size: 1.4rem;
+ white-space: nowrap;
+ overflow: hidden;
+ max-width: calc(100% - 4rem);
+ text-overflow: ellipsis;
+ display: block;
+ height: 100%;
+ line-height: 3.8rem;
+ }
+ }
+ &__placeholder {
+ position: absolute;
+ left: 1.1rem;
+ top: 1.1rem;
+ pointer-events: none;
+ transition: transform 0.25s linear;
+ padding: 0 0.4rem;
+ font-size: 1.4rem;
+ transform: none;
+ background-color: var(--fill-normal);
+ color: var(--text-less-prominent);
+ transform-origin: top left;
+ line-height: 1.43;
+ white-space: nowrap;
+ max-width: calc(100% - 3.6rem);
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+ &--has-value {
+ transform: translate(0, -1.8rem) scale(0.75);
+ color: var(--text-general);
+ max-width: 100%;
+ }
+ &--hide-top-placeholder {
+ transform: translate(0, -1.8rem) scale(0.75);
+ color: var(--text-general);
+ max-width: 100%;
+ display: none;
+ }
+ }
+ &__picker {
+ opacity: 0;
+ width: 100%;
+ height: 38px;
+ left: 0;
+ top: 0;
+ position: absolute;
+ }
+ &--disabled {
+ .dc-select-native__display-text {
+ color: var(--text-less-prominent);
+ }
+ .dc-select-native__placeholder:not(.dc-select-native__placeholder--has-value) {
+ color: var(--text-less-prominent);
+ }
+ .dc-icon {
+ --fill-color1: var(--text-less-prominent);
+ }
+ }
+ &--error {
+ .dc-select-native__placeholder {
+ color: var(--brand-red-coral);
+ }
+ }
+ &__hint {
+ margin-left: 1.2rem;
+ }
+ &__suffix-icon {
+ position: absolute;
+ left: 11px;
+ }
+}
diff --git a/src/components/shared_ui/select-native/select-native.tsx b/src/components/shared_ui/select-native/select-native.tsx
new file mode 100644
index 00000000..0df56abc
--- /dev/null
+++ b/src/components/shared_ui/select-native/select-native.tsx
@@ -0,0 +1,228 @@
+import React from 'react';
+import classNames from 'classnames';
+
+import { Text } from '@deriv-com/ui';
+
+import { Icon } from '@/utils/tmp/dummy';
+
+import Field from '../field/field';
+
+type TSelectNative = {
+ className?: string;
+ classNameDisplay?: string;
+ classNameHint?: string;
+ error?: string;
+ hint?: string;
+ label?: string;
+ placeholder?: string;
+ should_show_empty_option?: boolean;
+ suffix_icon?: string;
+ data_testid?: string;
+ hide_selected_value?: boolean;
+ hide_top_placeholder?: boolean;
+ value?: string | number;
+ list_items: Array | { [key: string]: Array };
+} & Omit &
+ Omit, 'value'>; // Default type of value in HTMLSelectElement is only string but here string | number is required
+
+type TSelectNativeOptions = {
+ list_items: Array;
+ should_hide_disabled_options?: boolean;
+ use_text?: boolean;
+};
+
+type TListItem = {
+ text: string;
+ value: string;
+ disabled?: boolean;
+ nativepicker_text?: React.ReactNode;
+ group?: string;
+ id?: string;
+};
+
+const getDisplayText = (list_items: Array | { [key: string]: Array }, value: string | number) => {
+ const dropdown_items = Array.isArray(list_items)
+ ? list_items
+ : ([] as Array).concat(...Object.values(list_items)); //typecasting since [] is inferred to be type never[]
+ const list_obj = dropdown_items.find(item =>
+ typeof item.value !== 'string'
+ ? item.value === value
+ : item.value.toLowerCase() === (value as string).toLowerCase()
+ );
+
+ if (list_obj) return list_obj.text;
+ return '';
+};
+
+const SelectNativeOptions = ({ list_items, should_hide_disabled_options, use_text }: TSelectNativeOptions) => {
+ const options = should_hide_disabled_options ? list_items.filter(opt => !opt.disabled) : list_items;
+ const has_group = Array.isArray(list_items) && !!list_items[0]?.group;
+
+ if (has_group) {
+ const dropdown_items = options.reduce((dropdown_map: { [key: string]: Array }, item) => {
+ if (item.group) {
+ const index = item.group;
+ dropdown_map[index] = dropdown_map[index] || [];
+ dropdown_map[index].push(item);
+ }
+ return dropdown_map;
+ }, {});
+ const group_names = Object.keys(dropdown_items);
+ return (
+
+ {group_names.map(option => (
+
+ {dropdown_items[option].map((value: TListItem) => (
+
+ {value.nativepicker_text || value.text}
+
+ ))}
+
+ ))}
+
+ );
+ }
+ return (
+
+ {options.map(option => (
+
+ {option.nativepicker_text || option.text}
+
+ ))}
+
+ );
+};
+
+const SelectNative = ({
+ className,
+ classNameDisplay,
+ classNameHint,
+ disabled,
+ error,
+ hide_selected_value,
+ hint,
+ label,
+ list_items,
+ placeholder,
+ should_hide_disabled_options = true,
+ should_show_empty_option = true,
+ suffix_icon,
+ use_text,
+ value,
+ data_testid,
+ hide_top_placeholder = false,
+ ...props
+}: TSelectNative) => (
+
+
+
+
+
+ {list_items && value && (
+