diff --git a/packages/appstore/src/components/modals/modal-manager.tsx b/packages/appstore/src/components/modals/modal-manager.tsx
index a80d73360f20..b35cec7b0fd9 100644
--- a/packages/appstore/src/components/modals/modal-manager.tsx
+++ b/packages/appstore/src/components/modals/modal-manager.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { ResetTradingPasswordModal } from '@deriv/account';
+import { useFeatureFlags } from '@deriv/hooks';
import { TTradingPlatformAvailableAccount } from './account-type-modal/types';
import MT5AccountTypeModal from './account-type-modal';
import RegulatorsCompareModal from './regulators-compare-modal';
@@ -18,6 +19,9 @@ import { TOpenAccountTransferMeta } from 'Types';
import { DetailsOfEachMT5Loginid } from '@deriv/api-types';
import FailedVerificationModal from './failed-veriification-modal';
import AccountTransferModal from 'Components/account-transfer-modal';
+import RealWalletsUpgrade from './real-wallets-upgrade/real-wallets-upgrade';
+import WalletsMigrationFailed from './wallets-migration-failed';
+import WalletModal from './wallet-modal';
type TCurrentList = DetailsOfEachMT5Loginid & {
enabled: number;
@@ -25,6 +29,7 @@ type TCurrentList = DetailsOfEachMT5Loginid & {
const ModalManager = () => {
const store = useStores();
+ const { is_wallet_enabled } = useFeatureFlags();
const { common, client, modules, traders_hub, ui } = store;
const {
is_logged_in,
@@ -53,7 +58,8 @@ const ModalManager = () => {
is_reset_trading_password_modal_visible,
setResetTradingPasswordModalOpen,
} = ui;
- const { is_demo, is_account_transfer_modal_open, toggleAccountTransferModal } = traders_hub;
+ const { is_demo, is_account_transfer_modal_open, toggleAccountTransferModal, is_real_wallets_upgrade_on } =
+ traders_hub;
const [password_manager, setPasswordManager] = React.useState<{
is_visible: boolean;
@@ -166,6 +172,9 @@ const ModalManager = () => {
toggleModal={toggleAccountTransferModal}
/>
+ {is_real_wallets_upgrade_on &&
}
+
+ {is_wallet_enabled &&
}
);
};
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/__tests__/real-wallets-upgrade.spec.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/__tests__/real-wallets-upgrade.spec.tsx
new file mode 100644
index 000000000000..e9d77292b34e
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/__tests__/real-wallets-upgrade.spec.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import RealWalletsUpgrade from '../real-wallets-upgrade';
+import { render } from '@testing-library/react';
+import { StoreProvider, mockStore } from '@deriv/stores';
+import { APIProvider } from '@deriv/api';
+
+describe('
', () => {
+ const wrapper = (mock: ReturnType
) => {
+ const Component = ({ children }: { children: JSX.Element }) => (
+
+ {children}
+
+ );
+ return Component;
+ };
+ it('should render the Modal', () => {
+ const mock = mockStore({
+ traders_hub: {
+ is_real_wallets_upgrade_on: true,
+ toggleWalletsUpgrade: true,
+ },
+ });
+
+ const { container } = render(, { wrapper: wrapper(mock) });
+
+ expect(container).toBeInTheDocument();
+ });
+
+ it('should not render the Modal if is_real_wallets_upgrade_on is false', () => {
+ const mock = mockStore({
+ traders_hub: {
+ is_real_wallets_upgrade_on: false,
+ toggleWalletsUpgrade: false,
+ },
+ });
+
+ const { container } = render(, { wrapper: wrapper(mock) });
+
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/desktop-real-wallets-upgrade.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/desktop-real-wallets-upgrade.tsx
new file mode 100644
index 000000000000..4bfa0ceb0c83
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/desktop-real-wallets-upgrade.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { Modal } from '@deriv/components';
+import { useStore, observer } from '@deriv/stores';
+import WalletsUpgradeFooter from './wallets-upgrade-footer';
+import WalletsUpgradeContent from './wallets-upgrade-content';
+import { TRealWalletsUpgradeSteps } from 'Types';
+
+const DesktopRealWalletsUpgrade = observer(({ wallet_upgrade_steps }: TRealWalletsUpgradeSteps) => {
+ const { traders_hub: is_real_wallets_upgrade_on } = useStore();
+
+ const { handleClose } = wallet_upgrade_steps;
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+export default DesktopRealWalletsUpgrade;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/index.ts b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/index.ts
new file mode 100644
index 000000000000..23ebba8288de
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/index.ts
@@ -0,0 +1,4 @@
+import DesktopRealWalletsUpgrade from './desktop-real-wallets-upgrade';
+import MobileRealWalletsUpgrade from './mobile-real-wallets-upgrade';
+
+export { DesktopRealWalletsUpgrade, MobileRealWalletsUpgrade };
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/mobile-real-wallets-upgrade.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/mobile-real-wallets-upgrade.tsx
new file mode 100644
index 000000000000..4991c830864f
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/mobile-real-wallets-upgrade.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { MobileDialog, Modal } from '@deriv/components';
+import { observer, useStore } from '@deriv/stores';
+import { TRealWalletsUpgradeSteps } from 'Types';
+import WalletsUpgradeContent from './wallets-upgrade-content';
+import WalletsUpgradeFooter from './wallets-upgrade-footer';
+
+const MobileRealWalletsUpgrade = observer(({ wallet_upgrade_steps }: TRealWalletsUpgradeSteps) => {
+ const { traders_hub: is_real_wallets_upgrade_on } = useStore();
+
+ return (
+ }
+ >
+
+
+
+
+ );
+});
+
+export default MobileRealWalletsUpgrade;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallet_steps.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallet_steps.tsx
new file mode 100644
index 000000000000..78b4b571ed05
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallet_steps.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { WalletsIntro } from '../wallets-intro';
+import { EndFooter, InitialFooter } from './wallets-upgrade-footer';
+import WalletLinkingStep from '../wallet-linking-step/wallet-linking-step';
+import ReadyToUpgradeWallets from '../ready-to-upgrade-wallets';
+import getMockWalletMigrationResponse from 'Constants/mock_wallet_migration_response';
+import { TWalletSteps } from 'Types';
+
+const WalletSteps = ({
+ handleBack,
+ handleClose,
+ handleNext,
+ is_disabled,
+ toggleCheckbox,
+ upgradeToWallets,
+}: TWalletSteps) => [
+ {
+ name: 'intro_wallets',
+ component: ,
+ footer: ,
+ },
+ {
+ name: 'intro_wallets',
+ component: ,
+ },
+ {
+ name: 'intro_wallets',
+ component: ,
+ },
+ {
+ name: 'linking_step',
+ component: ,
+ },
+ {
+ name: 'linking_step',
+ component: ,
+ },
+ {
+ name: 'linking_step',
+ component: ,
+ },
+ {
+ name: 'ready_to_upgrade',
+ component: ,
+ footer: ,
+ },
+];
+
+export default WalletSteps;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallets-upgrade-content.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallets-upgrade-content.tsx
new file mode 100644
index 000000000000..9549d9b3d97c
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallets-upgrade-content.tsx
@@ -0,0 +1,12 @@
+import WalletSteps from './wallet_steps';
+import { TRealWalletsUpgradeSteps } from 'Types';
+
+const WalletsUpgradeContent = ({ wallet_upgrade_steps }: TRealWalletsUpgradeSteps) => {
+ const wallet_steps_array = WalletSteps({ ...wallet_upgrade_steps });
+
+ const { current_step } = wallet_upgrade_steps;
+
+ return wallet_steps_array?.[current_step]?.component || wallet_steps_array?.[0].component;
+};
+
+export default WalletsUpgradeContent;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallets-upgrade-footer.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallets-upgrade-footer.tsx
new file mode 100644
index 000000000000..d23350524844
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/modal-elements/wallets-upgrade-footer.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { Button, Modal } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import WalletSteps from './wallet_steps';
+import { TRealWalletsUpgradeSteps } from 'Types';
+
+type TDefaultFooter = {
+ handleBack: () => void;
+ handleNext: () => void;
+};
+
+type TInitialFooter = {
+ handleClose: () => void;
+ handleNext: () => void;
+};
+
+type TEndFooter = {
+ handleBack: () => void;
+ is_disabled: boolean;
+ upgradeToWallets: (value: boolean) => void;
+};
+
+export const DefaultFooter = ({ handleBack, handleNext }: TDefaultFooter) => (
+
+
+
+
+);
+
+export const InitialFooter = ({ handleClose, handleNext }: TInitialFooter) => (
+
+
+
+
+);
+
+export const EndFooter = ({ handleBack, is_disabled, upgradeToWallets }: TEndFooter) => (
+
+
+
+
+);
+
+const WalletsUpgradeFooter = ({ wallet_upgrade_steps }: TRealWalletsUpgradeSteps) => {
+ const wallet_steps_array = WalletSteps({ ...wallet_upgrade_steps });
+
+ const { current_step, handleBack, handleNext } = wallet_upgrade_steps;
+
+ return (
+ wallet_steps_array?.[current_step]?.footer ||
+ );
+};
+
+export default WalletsUpgradeFooter;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/index.ts b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/index.ts
new file mode 100644
index 000000000000..0fee66b7ca8d
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/index.ts
@@ -0,0 +1,3 @@
+import ReadyToUpgradeWallets from './ready-to-upgrade-wallets';
+
+export default ReadyToUpgradeWallets;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/ready-to-upgrade-wallets.scss b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/ready-to-upgrade-wallets.scss
new file mode 100644
index 000000000000..a2a96aa021e9
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/ready-to-upgrade-wallets.scss
@@ -0,0 +1,48 @@
+.wallet-steps {
+ &__text {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ gap: 0.8rem;
+ }
+
+ &__image {
+ position: relative;
+ left: 1.5rem;
+ }
+
+ &__info-section {
+ padding: 1.6rem;
+ margin-top: 2.4rem;
+ background-color: var(--transparent-info);
+ border-radius: 0.8rem;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ gap: 0.8rem;
+
+ @include mobile {
+ margin-top: 1.6rem;
+ }
+
+ &-text {
+ display: grid;
+ grid-template-columns: 1.6rem 1fr;
+ grid-column-gap: 0.8rem;
+ align-items: center;
+ }
+ }
+
+ &__checkbox {
+ margin-top: 2.4rem;
+ display: flex;
+ justify-content: center;
+ .dc-checkbox__label {
+ @include mobile {
+ font-size: var(--text-size-xxs);
+ }
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/ready-to-upgrade-wallets.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/ready-to-upgrade-wallets.tsx
new file mode 100644
index 000000000000..197ee30202bd
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/ready-to-upgrade-wallets.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { Checkbox, Text, Icon } from '@deriv/components';
+import { Localize, localize } from '@deriv/translations';
+import { observer, useStore } from '@deriv/stores';
+import WalletsImage from 'Assets/svgs/wallets';
+import getUpgradeInformationList from 'Constants/upgrade-info-lists-config';
+import './ready-to-upgrade-wallets.scss';
+import { useContentFlag } from '@deriv/hooks';
+
+type TReadyToUpgradeWallets = {
+ value: boolean;
+ toggleCheckbox: () => void;
+};
+
+const ReadyToUpgradeWallets = observer(({ value, toggleCheckbox }: TReadyToUpgradeWallets) => {
+ const { ui } = useStore();
+ const { is_mobile } = ui;
+ const text_body_size = is_mobile ? 'xs' : 's';
+ const text_info_size = is_mobile ? 'xxs' : 'xs';
+ const form_line_height = is_mobile ? 'm' : 'l';
+
+ const { is_eu_demo, is_eu_real, is_low_risk_cr_eu } = useContentFlag();
+ const is_eu = is_eu_demo || is_eu_real || is_low_risk_cr_eu;
+
+ return (
+
+
+
+
+
+
+
+ }
+ />
+
+
+
+ {getUpgradeInformationList({ is_eu, text_info_size, form_line_height })
+ .filter(info => info.visibility)
+ .map(({ name, content }) => (
+
+
+
+ {content}
+
+
+ ))}
+
+
+
+ );
+});
+
+export default ReadyToUpgradeWallets;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/test/ready-to-upgrade-wallets.spec.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/test/ready-to-upgrade-wallets.spec.tsx
new file mode 100644
index 000000000000..40542909bddf
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/ready-to-upgrade-wallets/test/ready-to-upgrade-wallets.spec.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import ReadyToUpgradeWallets from '../ready-to-upgrade-wallets';
+import { StoreProvider, mockStore } from '@deriv/stores';
+
+describe('ReadyToUpgradeWallets', () => {
+ const containerReadyToUpgradeWallets = (mock: ReturnType) => {
+ const toggleCheckbox = jest.fn();
+ const wrapper = ({ children }: { children: JSX.Element }) => (
+ {children}
+ );
+
+ return render(, {
+ wrapper,
+ });
+ };
+
+ it('should render ReadyToUpgradeWallets component', () => {
+ const mock = mockStore({});
+ const { container } = containerReadyToUpgradeWallets(mock);
+
+ expect(container).toBeInTheDocument();
+ });
+
+ it('should render checkbox', () => {
+ const mock = mockStore({});
+ containerReadyToUpgradeWallets(mock);
+
+ expect(screen.getByRole('checkbox')).toBeInTheDocument();
+ });
+
+ it('should render proper info sections for non-eu user', () => {
+ const mock = mockStore({});
+ containerReadyToUpgradeWallets(mock);
+
+ expect(
+ screen.getByText(
+ 'During the upgrade, deposits, withdrawals, transfers, and adding new accounts will be unavailable.'
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Your open positions won't be affected and you can continue trading.")
+ ).toBeInTheDocument();
+ expect(screen.getByText(/you can use/i)).toBeInTheDocument();
+ expect(screen.getByText(/payment agents'/i)).toBeInTheDocument();
+ expect(
+ screen.getByText(/services to deposit by adding a Payment Agent Wallet after the upgrade./i)
+ ).toBeInTheDocument();
+ expect(screen.getByText(/Deriv P2P/)).toBeInTheDocument();
+ expect(screen.getByText(/is unavailable in Wallets at this time/i)).toBeInTheDocument();
+ });
+
+ describe('should render proper info sections for eu user with different content flags', () => {
+ const validateAssertion = () => {
+ expect(
+ screen.getByText(
+ 'During the upgrade, deposits, withdrawals, transfers, and adding new accounts will be unavailable.'
+ )
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Your open positions won't be affected and you can continue trading.")
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/you can use/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/payment agents'/i)).not.toBeInTheDocument();
+ expect(
+ screen.queryByText(/services to deposit by adding a Payment Agent Wallet after the upgrade./i)
+ ).not.toBeInTheDocument();
+ expect(screen.queryByText(/Deriv P2P/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/is unavailable in Wallets at this time/i)).not.toBeInTheDocument();
+ };
+
+ it('should render proper info sections for eu user with eu_demo content flag', () => {
+ const mock = mockStore({ traders_hub: { content_flag: 'eu_demo' } });
+ containerReadyToUpgradeWallets(mock);
+
+ validateAssertion();
+ });
+
+ it('should render proper info sections for eu user with eu_real content flag', () => {
+ const mock = mockStore({ traders_hub: { content_flag: 'eu_real' } });
+ containerReadyToUpgradeWallets(mock);
+
+ validateAssertion();
+ });
+
+ it('should render proper info sections for eu user with low_risk_cr_eu content flag', () => {
+ const mock = mockStore({ traders_hub: { content_flag: 'low_risk_cr_eu' } });
+ containerReadyToUpgradeWallets(mock);
+
+ validateAssertion();
+ });
+ });
+});
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-account/wallet-account.scss b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-account/wallet-account.scss
new file mode 100644
index 000000000000..079da1291c28
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-account/wallet-account.scss
@@ -0,0 +1,25 @@
+.wallet-account {
+ width: 28.8rem;
+ height: 4.8rem;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ border: 1px solid $color-grey-2;
+ border-radius: 0.8rem;
+
+ @include mobile {
+ width: 24.4rem;
+ }
+
+ &__details {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ margin-left: 1.6rem;
+ }
+
+ &__icon {
+ margin-left: 1.6rem;
+ }
+}
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-account/wallet-account.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-account/wallet-account.tsx
new file mode 100644
index 000000000000..e092d2cf7b7f
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-account/wallet-account.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { Icon, Text } from '@deriv/components';
+import './wallet-account.scss';
+
+type TWalletAccount = {
+ balance: number;
+ currency: string;
+ name: string;
+ icon: string;
+};
+
+const WalletAccount = ({ balance, name, currency, icon }: TWalletAccount) => (
+
+
+
+
+ {name}
+
+
+ {balance} {currency}
+
+
+
+);
+
+export default WalletAccount;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-link/wallet-link-wrapper.scss b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-link/wallet-link-wrapper.scss
new file mode 100644
index 000000000000..846a4867ed8c
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-link/wallet-link-wrapper.scss
@@ -0,0 +1,148 @@
+.wallet-link-wrapper {
+ display: grid;
+ grid-template-columns: repeat(3, auto);
+ align-items: center;
+ justify-content: flex-start;
+ gap: 1.6rem;
+
+ &__title {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28.8rem;
+
+ &-text {
+ width: fit-content;
+ padding: 0.4rem 0.8rem;
+ border-radius: $BORDER_RADIUS * 6;
+ text-align: center;
+ background-color: var(--general-section-2);
+
+ @include mobile {
+ background-color: var(--general-section-1);
+ }
+ }
+ }
+
+ @include mobile {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 1.2rem;
+ }
+
+ &__accounts {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1.6rem;
+
+ @include mobile {
+ gap: 0.8rem;
+
+ &-title {
+ width: 100%;
+ display: block;
+ border-radius: $BORDER_RADIUS_2 $BORDER_RADIUS_2 0 0;
+ }
+ }
+ }
+
+ &__link {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+
+ @include mobile {
+ width: 100%;
+ flex-direction: column;
+ }
+
+ &-bracket {
+ width: 1.5rem;
+ height: calc(100% - 4.8rem);
+ border: 1px solid var(--brand-red-coral);
+ border-left: none;
+
+ &--single {
+ height: 0;
+ border: none;
+ border-top: 1px solid var(--brand-red-coral);
+ }
+
+ @include mobile {
+ width: 24.4rem;
+ height: 0.8rem;
+ border-top: none;
+ }
+ }
+
+ &-icon {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 1.4rem;
+
+ @include mobile {
+ flex-direction: column;
+ gap: 0.6rem;
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ display: inline-block;
+ background: var(--brand-red-coral);
+ }
+
+ &:before {
+ width: 4.8rem;
+ height: 0.1rem;
+
+ @include mobile {
+ width: 0.1rem;
+ height: 1.6rem;
+ }
+ }
+
+ &:after {
+ width: 6.3rem;
+ height: 0.1rem;
+
+ @include mobile {
+ width: 0.1rem;
+ height: 2.4rem;
+ }
+ }
+ }
+ }
+
+ &__card-wrapper {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ &:after {
+ display: inline-block;
+ content: '';
+ }
+
+ &-title {
+ display: block;
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ border-radius: 0 0 $BORDER_RADIUS_2 $BORDER_RADIUS_2;
+ }
+
+ @include mobile {
+ display: block;
+ height: 14rem;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-link/wallet-link-wrapper.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-link/wallet-link-wrapper.tsx
new file mode 100644
index 000000000000..6602310076b8
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-link/wallet-link-wrapper.tsx
@@ -0,0 +1,73 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Icon, Text, WalletCard } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import WalletAccount from '../wallet-account/wallet-account';
+import { observer, useStore } from '@deriv/stores';
+import './wallet-link-wrapper.scss';
+
+export type TWalletLinkWrapper = {
+ wallet_details: React.ComponentProps['wallet'];
+ account_list: {
+ balance: number;
+ currency: string;
+ account_name: string;
+ icon: string;
+ }[];
+};
+
+const WalletLinkWrapper = observer(({ wallet_details, account_list }: TWalletLinkWrapper) => {
+ const { ui } = useStore();
+ const { is_mobile } = ui;
+ return (
+
+
+ {is_mobile && (
+
+
+
+ )}
+ {account_list.map(account => {
+ return (
+
+ );
+ })}
+
+
+
+
+ {is_mobile && (
+
+
+
+ )}
+
+
+ );
+});
+
+export default WalletLinkWrapper;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-linking-step/wallet-linking-step.scss b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-linking-step/wallet-linking-step.scss
new file mode 100644
index 000000000000..1f7a86dd5cd1
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-linking-step/wallet-linking-step.scss
@@ -0,0 +1,60 @@
+.wallet-linking-step {
+ width: 100%;
+ height: calc(57.4rem); // 73.4rem (modal-height) - 16rem (~15.2rem height of header + footer + margin from top)
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+
+ @include mobile {
+ height: 100%;
+ margin: 4rem 0 7.4rem;
+ }
+
+ &__description {
+ margin-top: 0.8rem;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ margin-top: 2rem;
+ padding: 0.4rem 1.6rem;
+ gap: 2.2rem;
+
+ @include mobile {
+ gap: 3.2rem;
+ padding: 0.4rem 3.8rem;
+ }
+ }
+
+ &__title-small {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ gap: 36.1rem;
+ margin-top: 5.6rem;
+
+ &-text {
+ padding: 0.4rem 0.8rem;
+ border-radius: $BORDER_RADIUS * 6;
+ }
+ }
+
+ &__note {
+ margin-top: 2.4rem;
+ padding: 0.8rem 1.6rem;
+ border-radius: $BORDER_RADIUS;
+ }
+
+ &__title-text {
+ width: fit-content;
+ background-color: var(--general-section-2);
+
+ @include mobile {
+ background-color: var(--general-section-1);
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-linking-step/wallet-linking-step.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-linking-step/wallet-linking-step.tsx
new file mode 100644
index 000000000000..33fb901133a9
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallet-linking-step/wallet-linking-step.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { Text, ThemedScrollbars } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import { observer, useStore } from '@deriv/stores';
+import WalletLinkWrapper, { TWalletLinkWrapper } from '../wallet-link/wallet-link-wrapper';
+import './wallet-linking-step.scss';
+
+type TWalletLinkingStep = {
+ data: { title: string; wallets: TWalletLinkWrapper[] };
+};
+
+const WalletLinkingStep = observer(({ data }: TWalletLinkingStep) => {
+ const { ui } = useStore();
+ const { is_mobile } = ui;
+ return (
+
+
+ {data.title}
+
+
+
+
+
+
+
+ {!is_mobile && (
+
+
+
+
+
+
+
+
+ )}
+
+ {data.wallets.map(({ wallet_details, account_list }) => {
+ return (
+
+ );
+ })}
+
+
+ );
+});
+
+export default WalletLinkingStep;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/__tests__/wallets-intro.spec.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/__tests__/wallets-intro.spec.tsx
new file mode 100644
index 000000000000..98b2c65a8a62
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/__tests__/wallets-intro.spec.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import WalletsImage from 'Assets/svgs/wallets';
+import { WalletsIntroComponent } from '../wallets-intro';
+import { render, screen } from '@testing-library/react';
+import { StoreProvider, mockStore } from '@deriv/stores';
+
+let mock = mockStore({});
+
+const mocked_props = {
+ image: ,
+ title: 'Upgrade To Wallets',
+ description: 'A better way to manage your funds',
+ bullets: ['Bullet 1', 'Bullet 2', 'Bullet 3'],
+};
+
+const checkContainerWalletsIntroComponent = () => {
+ const wrapper = ({ children }: { children: JSX.Element }) => {children};
+
+ const { container } = render(, {
+ wrapper,
+ });
+ expect(container).toBeInTheDocument();
+};
+
+describe('WalletsIntroComponent', () => {
+ beforeEach(() => {
+ mock = mockStore({});
+ });
+ it('should render Wallet Intro Component', () => {
+ checkContainerWalletsIntroComponent();
+ });
+
+ it('should render icon', () => {
+ checkContainerWalletsIntroComponent();
+ expect(screen.queryByTestId('dt_how_it_works')).toBeInTheDocument();
+ });
+
+ it('should render title, description and bullets', () => {
+ checkContainerWalletsIntroComponent();
+ expect(screen.getByText(mocked_props.title)).toBeInTheDocument();
+ expect(screen.getByText(mocked_props.description)).toBeInTheDocument();
+ mocked_props.bullets.forEach(bullet => {
+ expect(screen.getByText(bullet)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/index.ts b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/index.ts
new file mode 100644
index 000000000000..4cfcc528ec65
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/index.ts
@@ -0,0 +1,3 @@
+import { WalletsIntro, WalletsIntroComponent } from './wallets-intro';
+
+export { WalletsIntro, WalletsIntroComponent };
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/wallets-intro.scss b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/wallets-intro.scss
new file mode 100644
index 000000000000..3f94b7c93e78
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/wallets-intro.scss
@@ -0,0 +1,54 @@
+.wallet-steps {
+ &__title {
+ padding-top: 2rem;
+
+ @include mobile {
+ font-size: var(--text-size-m);
+ }
+ }
+
+ &__description {
+ padding: 0.5rem 0 1.5rem;
+
+ @include mobile {
+ font-size: var(--text-size-sm);
+ }
+ }
+
+ &__bullet {
+ display: flex;
+ align-items: center;
+ padding-bottom: 1rem;
+ text-align: center;
+ justify-content: center;
+
+ @include mobile {
+ justify-content: left;
+ }
+
+ &-points {
+ display: grid;
+ grid-template-columns: 1.6rem 1fr;
+ align-items: center;
+ gap: 0.8rem;
+
+ @include mobile {
+ align-items: baseline;
+ }
+ }
+
+ @include mobile {
+ justify-content: left;
+ }
+
+ &-text {
+ @include mobile {
+ text-align: left;
+ }
+ }
+
+ &-icon {
+ margin-right: 1rem;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/wallets-intro.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/wallets-intro.tsx
new file mode 100644
index 000000000000..9742af03357a
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/components/wallets-intro/wallets-intro.tsx
@@ -0,0 +1,92 @@
+import React from 'react';
+import { Text, Icon } from '@deriv/components';
+import { observer, useStore } from '@deriv/stores';
+import getWalletsIntroContent from 'Constants/wallets-intro-content-config';
+import { useContentFlag } from '@deriv/hooks';
+import './wallets-intro.scss';
+
+type TWalletsIntro = {
+ title: string;
+ description: string;
+ bullets: string[];
+ eu_user?: boolean;
+ image?: React.ReactNode;
+};
+
+type TWalletsIntroComponent = {
+ current_step: number;
+};
+
+const WalletsIntroComponent = observer(({ image, title, description, bullets }: TWalletsIntro) => {
+ const { ui } = useStore();
+ const { is_mobile } = ui;
+
+ const text_title_size = is_mobile ? 'xsm' : 'l';
+ const text_body_size = is_mobile ? 's' : 'm';
+ const text_info_size = is_mobile ? 'xs' : 's';
+ const form_line_height = is_mobile ? 'm' : 'l';
+
+ return (
+
+ {image}
+
+ {title}
+
+
+ {description}
+
+ {bullets.map(bullet => (
+
+ {bullet && (
+
+
+
+ {bullet}
+
+
+ )}
+
+ ))}
+
+ );
+});
+
+const WalletsIntro = ({ current_step }: TWalletsIntroComponent) => {
+ const { is_eu_demo, is_eu_real, is_low_risk_cr_eu } = useContentFlag();
+ const is_eu = is_eu_demo || is_eu_real || is_low_risk_cr_eu;
+ const step = getWalletsIntroContent(is_eu)?.[current_step] || [];
+
+ return (
+
+
+
+ );
+};
+
+export { WalletsIntro, WalletsIntroComponent };
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/index.ts b/packages/appstore/src/components/modals/real-wallets-upgrade/index.ts
new file mode 100644
index 000000000000..dc268ba30609
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/index.ts
@@ -0,0 +1,3 @@
+import RealWalletsUpgrade from './real-wallets-upgrade';
+
+export default RealWalletsUpgrade;
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/real-wallets-upgrade.scss b/packages/appstore/src/components/modals/real-wallets-upgrade/real-wallets-upgrade.scss
new file mode 100644
index 000000000000..2fc3b82c960b
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/real-wallets-upgrade.scss
@@ -0,0 +1,34 @@
+.wallet-steps {
+ width: 100%;
+ height: 100%;
+ display: flex;
+
+ @include mobile {
+ align-items: center;
+ }
+
+ &__content {
+ max-width: 77.6rem;
+ margin: 5rem auto 0;
+ text-align: center;
+
+ @include mobile {
+ margin: 2rem auto 8rem;
+ }
+ }
+
+ &__footer {
+ width: 100%;
+ position: sticky;
+ bottom: 0;
+ background-color: inherit;
+
+ @include mobile {
+ justify-content: center;
+ background: var(--fill-normal);
+ &-button {
+ width: 50%;
+ }
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/real-wallets-upgrade/real-wallets-upgrade.tsx b/packages/appstore/src/components/modals/real-wallets-upgrade/real-wallets-upgrade.tsx
new file mode 100644
index 000000000000..32f6d224104a
--- /dev/null
+++ b/packages/appstore/src/components/modals/real-wallets-upgrade/real-wallets-upgrade.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { useWalletMigration } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import { DesktopRealWalletsUpgrade, MobileRealWalletsUpgrade } from './components/modal-elements';
+import './real-wallets-upgrade.scss';
+
+const RealWalletsUpgrade = observer(() => {
+ const { traders_hub, ui } = useStore();
+ const { is_real_wallets_upgrade_on, toggleWalletsUpgrade } = traders_hub;
+ const { is_mobile } = ui;
+
+ const [current_step, setCurrentStep] = React.useState(0);
+ const [is_disabled, setIsDisabled] = React.useState(false);
+
+ React.useEffect(() => {
+ if (!is_real_wallets_upgrade_on) {
+ setCurrentStep(0);
+ setIsDisabled(false);
+ }
+ }, [is_real_wallets_upgrade_on]);
+
+ const handleNext = () => setCurrentStep(prev_step => prev_step + 1);
+
+ const handleBack = () => setCurrentStep(prev_step => prev_step - 1);
+
+ const handleClose = () => toggleWalletsUpgrade(false);
+
+ const { start_migration } = useWalletMigration();
+
+ const upgradeToWallets = () => {
+ start_migration();
+ toggleWalletsUpgrade(false);
+ };
+
+ const toggleCheckbox = () => {
+ setIsDisabled(prevDisabled => !prevDisabled);
+ };
+
+ const wallet_upgrade_steps = {
+ current_step,
+ handleBack,
+ handleClose,
+ handleNext,
+ is_disabled,
+ toggleCheckbox,
+ upgradeToWallets,
+ };
+
+ return (
+
+ {is_mobile ? (
+
+ ) : (
+
+ )}
+
+ );
+});
+
+export default RealWalletsUpgrade;
diff --git a/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-body.spec.tsx b/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-body.spec.tsx
new file mode 100644
index 000000000000..490a3e1efb79
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-body.spec.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { BrowserRouter } from 'react-router-dom';
+import WalletModalBody from '../wallet-modal-body';
+import { mockStore, StoreProvider } from '@deriv/stores';
+
+jest.mock('Components/wallet-transfer', () => jest.fn(() => WalletTransfer
));
+jest.mock('Components/transaction-list', () => jest.fn(() => Transactions
));
+jest.mock('Components/wallet-deposit', () => jest.fn(() => Deposit
));
+
+describe('WalletModalBody', () => {
+ let mocked_props: React.ComponentProps;
+
+ beforeEach(() => {
+ mocked_props = {
+ contentScrollHandler: jest.fn(),
+ is_dark: false,
+ is_mobile: false,
+ is_wallet_name_visible: true,
+ setIsWalletNameVisible: jest.fn(),
+ wallet: {
+ balance: 1000,
+ currency: 'USD',
+ currency_config: {
+ display_code: 'USD',
+ is_crypto: false,
+ } as typeof mocked_props['wallet']['currency_config'],
+ gradient_card_class: 'wallet-card__usd',
+ gradient_header_class: 'wallet-header__usd',
+ icon: '',
+ is_demo: true,
+ is_disabled: 0,
+ is_malta_wallet: false,
+ is_selected: true,
+ is_virtual: 1,
+ landing_company_name: 'svg',
+ wallet_currency_type: 'Demo',
+ },
+ };
+ });
+
+ const renderWithRouter = (component: JSX.Element) => {
+ render({component});
+ };
+
+ it('Should render proper tabs for demo wallet', () => {
+ const mocked_store = mockStore({
+ traders_hub: {
+ active_modal_tab: 'Transfer',
+ },
+ });
+ renderWithRouter(
+
+
+
+ );
+
+ expect(screen.getByText('Transfer')).toBeInTheDocument();
+ expect(screen.getByText('Transactions')).toBeInTheDocument();
+ expect(screen.getByText('Reset balance')).toBeInTheDocument();
+ });
+
+ it('Should render proper content under the Transfer tab', () => {
+ const mocked_store = mockStore({
+ traders_hub: {
+ active_modal_tab: 'Transfer',
+ },
+ });
+ renderWithRouter(
+
+
+
+ );
+
+ const el_transfer_tab = screen.getByText('Transfer');
+ userEvent.click(el_transfer_tab);
+
+ expect(screen.getByText('WalletTransfer')).toBeInTheDocument();
+ });
+
+ it('Should trigger setWalletModalActiveTab callback when the user clicked on the tab', () => {
+ mocked_props.wallet.is_demo = false;
+ const mocked_store = mockStore({
+ traders_hub: {
+ active_modal_tab: 'Deposit',
+ },
+ });
+ renderWithRouter(
+
+
+
+ );
+
+ const el_transactions_tab = screen.getByText('Transactions');
+ userEvent.click(el_transactions_tab);
+
+ expect(mocked_store.traders_hub.setWalletModalActiveTab).toHaveBeenCalledTimes(1);
+ });
+
+ it('Should trigger contentScrollHandler callback when the user scrolls the content', () => {
+ mocked_props.wallet.is_demo = false;
+ const mocked_store = mockStore({
+ traders_hub: {
+ active_modal_tab: 'Deposit',
+ },
+ });
+ renderWithRouter(
+
+
+
+ );
+
+ const el_themed_scrollbars = screen.getByTestId('dt_themed_scrollbars');
+ fireEvent.scroll(el_themed_scrollbars);
+
+ expect(mocked_props.contentScrollHandler).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-header.spec.tsx b/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-header.spec.tsx
new file mode 100644
index 000000000000..c6911fe75bce
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-header.spec.tsx
@@ -0,0 +1,79 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import WalletModalHeader from '../wallet-modal-header';
+
+jest.mock('@deriv/hooks', () => ({
+ useCurrencyConfig: () => ({ getConfig: () => ({ display_code: 'USD' }) }),
+}));
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useFetch: jest.fn(() => ({
+ data: {
+ website_status: {
+ currencies_config: {
+ USD: {
+ fractional_digits: 2,
+ is_deposit_suspended: 0,
+ is_suspended: 0,
+ is_withdrawal_suspended: 0,
+ name: 'US Dollar',
+ stake_default: 10,
+ type: 'fiat',
+ },
+ },
+ },
+ },
+ })),
+}));
+
+describe('WalletModalHeader', () => {
+ let mocked_props: React.ComponentProps;
+
+ beforeEach(() => {
+ mocked_props = {
+ closeModal: jest.fn(),
+ is_dark: false,
+ is_mobile: false,
+ is_wallet_name_visible: true,
+ wallet: {
+ balance: 1000,
+ currency: 'USD',
+ currency_config: {
+ display_code: 'USD',
+ is_crypto: false,
+ } as typeof mocked_props['wallet']['currency_config'],
+ gradient_card_class: 'wallet-card__usd',
+ gradient_header_class: 'wallet-header__usd',
+ icon: 'IcWalletIcon',
+ is_demo: true,
+ is_disabled: 0,
+ is_malta_wallet: false,
+ is_selected: true,
+ is_virtual: 1,
+ landing_company_name: 'svg',
+ wallet_currency_type: 'USD',
+ },
+ };
+ });
+
+ it('Should render header with proper title, balance, badge and icons', () => {
+ render();
+
+ expect(screen.getByText('USD Wallet')).toBeInTheDocument();
+ expect(screen.getByText('Demo')).toBeInTheDocument();
+ expect(screen.getByText('1,000.00 USD')).toBeInTheDocument();
+ expect(screen.getByTestId('dt_wallet_icon')).toBeInTheDocument();
+ expect(screen.getByTestId('dt_close_icon')).toBeInTheDocument();
+ });
+
+ it('Should trigger onClose callback when the user clicked on the cross close button', () => {
+ render();
+
+ const el_close_btn = screen.getByTestId('dt_close_icon');
+ userEvent.click(el_close_btn);
+
+ expect(mocked_props.closeModal).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal.spec.tsx b/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal.spec.tsx
new file mode 100644
index 000000000000..741468feae89
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal.spec.tsx
@@ -0,0 +1,66 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import WalletModal from '../wallet-modal';
+import { useActiveWallet } from '@deriv/hooks';
+import { APIProvider } from '@deriv/api';
+
+jest.mock('../wallet-modal-header', () => jest.fn(() => WalletModalHeader
));
+jest.mock('../wallet-modal-body', () => jest.fn(() => WalletModalBody
));
+
+jest.mock('@deriv/hooks', () => ({
+ ...jest.requireActual('@deriv/hooks'),
+ useActiveWallet: jest.fn(),
+}));
+
+const mockUseActiveWallet = useActiveWallet as jest.MockedFunction;
+
+describe('WalletModal', () => {
+ let modal_root_el: HTMLDivElement;
+ beforeAll(() => {
+ modal_root_el = document.createElement('div');
+ modal_root_el.setAttribute('id', 'modal_root');
+ document.body.appendChild(modal_root_el);
+ });
+
+ it('Should render cashier modal if is_wallet_modal_visible is true', () => {
+ const mocked_store = mockStore({
+ ui: { is_wallet_modal_visible: true },
+ client: { is_authorize: true },
+ traders_hub: { active_modal_wallet_id: 'CRW000000' },
+ });
+
+ // @ts-expect-error need to come up with a way to mock the return type of useFetch
+ mockUseActiveWallet.mockReturnValue({ loginid: 'CRW000000', is_demo: false });
+
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getByText('WalletModalHeader')).toBeInTheDocument();
+ expect(screen.getByText('WalletModalBody')).toBeInTheDocument();
+ });
+
+ it('Should not render cashier modal and show loader if authorize is false', () => {
+ const mocked_store = mockStore({
+ ui: { is_wallet_modal_visible: true },
+ client: { is_authorize: false },
+ traders_hub: { active_modal_wallet_id: 'CRW000000' },
+ });
+
+ // @ts-expect-error need to come up with a way to mock the return type of useFetch
+ mockUseActiveWallet.mockReturnValue({ loginid: 'CRW100000', is_demo: false });
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByTestId('dt_initial_loader')).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/modals/wallet-modal/index.ts b/packages/appstore/src/components/modals/wallet-modal/index.ts
new file mode 100644
index 000000000000..b3326d3837de
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/index.ts
@@ -0,0 +1,4 @@
+import WalletModal from './wallet-modal';
+import './wallet-modal.scss';
+
+export default WalletModal;
diff --git a/packages/appstore/src/components/modals/wallet-modal/provider.tsx b/packages/appstore/src/components/modals/wallet-modal/provider.tsx
new file mode 100644
index 000000000000..ed32b044f676
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/provider.tsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import { localize } from '@deriv/translations';
+import DemoResetBalance from 'Components/demo-reset-balance';
+import TransactionList from 'Components/transaction-list';
+import WalletDeposit from 'Components/wallet-deposit';
+import WalletTransfer from 'Components/wallet-transfer';
+import WalletWithdrawal from '../../wallet-withdrawal';
+
+export type TWalletType = 'real' | 'demo' | 'p2p' | 'payment_agent';
+
+export const getCashierOptions = (type: TWalletType) => {
+ switch (type) {
+ case 'real':
+ return [
+ {
+ icon: 'IcAdd',
+ label: localize('Deposit'),
+ content: () => ,
+ },
+ { icon: 'IcMinus', label: localize('Withdraw'), content: () => },
+ {
+ icon: 'IcAccountTransfer',
+ label: localize('Transfer'),
+ content: (props: React.ComponentProps) => ,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('Transactions'),
+ content: () => ,
+ },
+ ];
+ case 'demo':
+ return [
+ {
+ icon: 'IcAccountTransfer',
+ label: localize('Transfer'),
+ content: (props: React.ComponentProps) => ,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('Transactions'),
+ content: () => ,
+ },
+ {
+ icon: 'IcAdd',
+ label: localize('Reset balance'),
+ content: (props: React.ComponentProps) => ,
+ },
+ ];
+ case 'p2p':
+ return [
+ {
+ icon: 'IcAdd',
+ label: localize('Buy/Sell'),
+ content: () => Transfer Real
,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('Orders'),
+ content: () => Transfer Real
,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('My ads'),
+ content: () => Transfer Real
,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('My profile'),
+ content: () => Transfer Real
,
+ },
+ {
+ icon: 'IcAccountTransfer',
+ label: localize('Transfer'),
+ content: () => Transfer Real
,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('Transactions'),
+ content: () => Transfer Real
,
+ },
+ ];
+ case 'payment_agent':
+ return [
+ { icon: 'IcAdd', label: localize('Deposit'), content: () => Transfer Real
},
+ { icon: 'IcMinus', label: localize('Withdraw'), content: () => Transfer Real
},
+ {
+ icon: 'IcAccountTransfer',
+ label: localize('Transfer'),
+ content: () => Transfer Real
,
+ },
+ {
+ icon: 'IcStatement',
+ label: localize('Transactions'),
+ content: () => Transfer Real
,
+ },
+ ];
+ default:
+ return [];
+ }
+};
diff --git a/packages/appstore/src/components/modals/wallet-modal/wallet-modal-body.tsx b/packages/appstore/src/components/modals/wallet-modal/wallet-modal-body.tsx
new file mode 100644
index 000000000000..828324573ce5
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/wallet-modal-body.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Tabs, ThemedScrollbars, Div100vhContainer } from '@deriv/components';
+import { getCashierOptions } from './provider';
+import { observer, useStore } from '@deriv/stores';
+import type { TWalletAccount } from 'Types';
+
+type TWalletModalBodyProps = {
+ contentScrollHandler: React.UIEventHandler;
+ is_dark: boolean;
+ is_mobile: boolean;
+ setIsWalletNameVisible: (value: boolean) => void;
+ is_wallet_name_visible: boolean;
+ wallet: TWalletAccount;
+};
+
+const real_tabs = {
+ Deposit: 0,
+ Withdraw: 1,
+ Transfer: 2,
+ Transactions: 3,
+} as const;
+
+const demo_tabs = {
+ Deposit: 2,
+ Transfer: 0,
+ Transactions: 1,
+ Withdraw: undefined,
+} as const;
+
+const WalletModalBody = observer(
+ ({
+ contentScrollHandler,
+ is_dark,
+ is_mobile,
+ setIsWalletNameVisible,
+ is_wallet_name_visible,
+ wallet,
+ }: TWalletModalBodyProps) => {
+ const store = useStore();
+
+ const { is_demo } = wallet;
+
+ const {
+ traders_hub: { active_modal_tab, setWalletModalActiveTab },
+ } = store;
+
+ const getHeightOffset = React.useCallback(() => {
+ const desktop_header_height = '24.4rem';
+ const mobile_header_height = '8.2rem';
+
+ return is_mobile ? mobile_header_height : desktop_header_height;
+ }, [is_mobile]);
+
+ const tabs = is_demo ? demo_tabs : real_tabs;
+
+ return (
+ {
+ const tab_name = Object.keys(tabs).find(
+ key => tabs[key as keyof typeof tabs] === index
+ ) as typeof active_modal_tab;
+ setWalletModalActiveTab(tab_name);
+ }}
+ >
+ {getCashierOptions(is_demo ? 'demo' : 'real').map(option => {
+ return (
+
+
+
+
+ {option.content({
+ is_wallet_name_visible,
+ contentScrollHandler,
+ setIsWalletNameVisible,
+ })}
+
+
+
+
+ );
+ })}
+
+ );
+ }
+);
+
+export default WalletModalBody;
diff --git a/packages/appstore/src/components/modals/wallet-modal/wallet-modal-header.tsx b/packages/appstore/src/components/modals/wallet-modal/wallet-modal-header.tsx
new file mode 100644
index 000000000000..ed4613c80cfc
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/wallet-modal-header.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Icon, Text, WalletIcon } from '@deriv/components';
+import { formatMoney } from '@deriv/shared';
+import { getAccountName } from 'Constants/utils';
+import { WalletJurisdictionBadge } from 'Components/wallet-jurisdiction-badge';
+import type { TWalletAccount } from 'Types';
+
+type TWalletModalHeaderProps = {
+ closeModal: VoidFunction;
+ is_dark: boolean;
+ is_mobile: boolean;
+ is_wallet_name_visible: boolean;
+ wallet: TWalletAccount;
+};
+
+const WalletModalHeader = ({
+ closeModal,
+ is_dark,
+ is_mobile,
+ is_wallet_name_visible,
+ wallet,
+}: TWalletModalHeaderProps) => {
+ const { balance, currency, icon, currency_config, is_demo, gradient_header_class, landing_company_name } = wallet;
+ const is_crypto = currency_config?.is_crypto;
+ const display_currency_code = currency_config?.display_code;
+
+ const header_class_name = 'wallet-modal--header';
+
+ const getCloseIcon = React.useCallback(() => {
+ if (is_demo && is_dark) return 'IcAppstoreCloseLight';
+ if (is_demo && !is_dark) return 'IcAppstoreCloseDark';
+ if (is_dark) return 'IcAppstoreCloseDark';
+ return 'IcAppstoreCloseLight';
+ }, [is_dark, is_demo]);
+
+ const getWalletIcon = React.useCallback(() => {
+ if (currency && ['USDT', 'eUSDT', 'tUSDT', 'UST'].includes(currency)) {
+ return is_dark ? 'IcWalletModalTetherDark' : 'IcWalletModalTetherLight';
+ }
+ return icon;
+ }, [currency, icon, is_dark]);
+
+ const getStylesByClassName = (class_name: string) => {
+ return classNames(class_name, {
+ [`${class_name}-demo`]: is_demo,
+ });
+ };
+
+ const getWalletIconType = (): React.ComponentProps['type'] => {
+ if (is_demo) return 'demo';
+ return is_crypto ? 'crypto' : 'fiat';
+ };
+
+ const getWalletIconSize = (): React.ComponentProps['size'] => {
+ if (is_mobile) return is_demo || is_crypto ? 'large' : 'xlarge';
+ return 'xxlarge';
+ };
+
+ return (
+
+
+
+
+
+ {getAccountName({
+ display_currency_code: wallet.currency_config?.display_code,
+ account_type: 'wallet',
+ })}
+
+
+
+
+ {formatMoney(currency || '', balance, true)} {display_currency_code}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default WalletModalHeader;
diff --git a/packages/appstore/src/components/modals/wallet-modal/wallet-modal.scss b/packages/appstore/src/components/modals/wallet-modal/wallet-modal.scss
new file mode 100644
index 000000000000..043ee42f64c0
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/wallet-modal.scss
@@ -0,0 +1,218 @@
+.dc-modal__container_wallet-modal {
+ display: flex;
+ align-items: center;
+ position: fixed;
+ inset: 0;
+ min-height: calc(100vh - 84px) !important;
+ min-width: 100vw !important;
+ margin: 4.8rem 0 3.6rem;
+ border-radius: unset;
+ background-color: var(--general-main-1);
+ z-index: 9997;
+ box-shadow: none;
+
+ @include mobile {
+ margin: 0;
+ }
+
+ // styles for mobile and desktop modal body (tabs with content)
+ .dc-tabs {
+ &--wallet-modal {
+ width: 100%;
+ margin-top: -4.8rem;
+ z-index: 9999;
+
+ @include mobile {
+ top: 8.2rem;
+ margin-top: 0;
+ transition: top 0.2s ease;
+
+ &.is_scrolled {
+ top: 4.2rem;
+ }
+ }
+
+ &-themed-scrollbar {
+ width: 100%;
+ }
+
+ &-content-wrapper {
+ max-width: 128rem;
+ margin: 0 auto;
+ padding: 2.4rem 4rem;
+
+ @include mobile {
+ padding: 1.6rem;
+ }
+ }
+ }
+ &__list {
+ padding: 0 4rem;
+
+ @include mobile {
+ padding: 0 1.6rem;
+ }
+
+ &--wallet-modal {
+ max-width: 128rem;
+ width: 100%;
+ margin: 0 auto;
+
+ @include mobile {
+ width: 100%;
+ z-index: 3;
+ overflow-x: scroll;
+
+ /* IE and Edge */
+ -ms-overflow-style: none;
+ /* Firefox */
+ scrollbar-width: none;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ display: none;
+ }
+
+ &.is_scrolled {
+ top: 4.4rem;
+ }
+ }
+ }
+ }
+
+ &__item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 3.2rem;
+ height: 4.8rem;
+
+ @include mobile {
+ padding: 0 1.6rem;
+ height: 4rem;
+ font-size: var(--text-size-xxs);
+ }
+
+ &__icon {
+ padding: 0;
+ margin-right: 0.8rem;
+ }
+ }
+
+ &__active {
+ background-color: var(--general-main-1);
+ border-radius: 1.6rem 1.6rem 0 0;
+ }
+
+ &__content {
+ width: 100%;
+ font-size: var(--text-size-l);
+ color: var(--text-prominent);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ @include mobile {
+ font-size: var(--text-size-xs);
+ }
+ }
+ }
+}
+
+.wallet-modal--header {
+ max-width: 128rem;
+ width: 100%;
+ display: flex;
+ position: relative;
+ height: 16rem;
+ padding: 2.4rem 4rem 7.2rem;
+
+ @include mobile {
+ height: 12.2rem;
+ padding: 1.6rem 1.6rem 5.6rem;
+ transition: height 0.2s ease;
+
+ .title-visibility {
+ height: 2rem;
+ }
+
+ .title-visibility,
+ .icon-visibility {
+ visibility: visible;
+ transition: visibility 0s, height 0.2s ease;
+ }
+ }
+
+ //TODO: check do we need this after bg change to radial-gradient
+ &__title-wrapper {
+ position: relative;
+ }
+
+ &__title {
+ display: flex;
+ align-items: center;
+
+ &-wallet {
+ padding-right: 0.8rem;
+ color: var(--text-general);
+
+ &-demo {
+ color: var(--demo-text-color-1);
+ }
+ }
+
+ &-balance {
+ color: var(--text-prominent);
+
+ &-demo {
+ color: var(--demo-text-color-2);
+ }
+ }
+ }
+
+ &--hidden-title {
+ height: 8.2rem;
+ align-items: center;
+ justify-content: space-between;
+
+ .title-visibility,
+ .icon-visibility {
+ visibility: hidden;
+ height: 0;
+ }
+ }
+
+ &__currency-icon {
+ z-index: 3;
+ margin-left: auto;
+ margin-right: 1.6rem;
+
+ @include mobile {
+ margin-right: 0.8rem;
+ }
+ }
+
+ &__close-icon {
+ position: relative;
+
+ .dc-icon {
+ cursor: pointer;
+ }
+ }
+
+ &-background {
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+
+ @include mobile {
+ position: fixed;
+ z-index: 3;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/wallet-modal/wallet-modal.tsx b/packages/appstore/src/components/modals/wallet-modal/wallet-modal.tsx
new file mode 100644
index 000000000000..0bcadb75798c
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallet-modal/wallet-modal.tsx
@@ -0,0 +1,80 @@
+import React, { useEffect } from 'react';
+import { Loading, Modal } from '@deriv/components';
+import { useActiveWallet } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import WalletModalHeader from './wallet-modal-header';
+import WalletModalBody from './wallet-modal-body';
+
+const WalletModal = observer(() => {
+ const store = useStore();
+
+ const {
+ client: { is_authorize, switchAccount },
+ ui: { is_dark_mode_on, is_wallet_modal_visible, is_mobile, setIsWalletModalVisible },
+ traders_hub: { active_modal_tab, active_modal_wallet_id, setWalletModalActiveTab },
+ } = store;
+
+ const active_wallet = useActiveWallet();
+
+ useEffect(() => {
+ let timeout_id: NodeJS.Timeout;
+
+ if (is_wallet_modal_visible && active_wallet?.loginid !== active_modal_wallet_id) {
+ /** Adding a delay as per requirement because the modal must appear first, then switch the account */
+ timeout_id = setTimeout(() => switchAccount(active_modal_wallet_id), 500);
+ }
+
+ return () => clearTimeout(timeout_id);
+ }, [active_modal_wallet_id, active_wallet?.loginid, is_wallet_modal_visible, switchAccount]);
+
+ const [is_wallet_name_visible, setIsWalletNameVisible] = React.useState(true);
+
+ React.useEffect(() => {
+ return setIsWalletNameVisible(true);
+ }, [active_modal_tab, is_wallet_modal_visible]);
+
+ const closeModal = () => {
+ setIsWalletModalVisible(false);
+ setWalletModalActiveTab(active_modal_tab);
+ };
+
+ const contentScrollHandler = React.useCallback(
+ (e: React.UIEvent) => {
+ if (is_mobile && is_wallet_modal_visible) {
+ const target = e.target as HTMLDivElement;
+ setIsWalletNameVisible(target.scrollTop <= 0);
+ }
+ },
+ [is_mobile, is_wallet_modal_visible]
+ );
+
+ const is_loading = active_wallet?.loginid !== active_modal_wallet_id || !is_authorize || !active_wallet;
+
+ return (
+
+ {is_loading ? (
+
+ ) : (
+
+
+
+
+ )}
+
+ );
+});
+
+export default WalletModal;
diff --git a/packages/appstore/src/components/modals/wallets-migration-failed/__tests__/wallets-migration-failed.spec.tsx b/packages/appstore/src/components/modals/wallets-migration-failed/__tests__/wallets-migration-failed.spec.tsx
new file mode 100644
index 000000000000..764d74276ff2
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallets-migration-failed/__tests__/wallets-migration-failed.spec.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { StoreProvider, mockStore } from '@deriv/stores';
+import WalletsMigrationFailed from '../wallets-migration-failed';
+
+jest.mock('@deriv/components', () => ({
+ ...jest.requireActual('@deriv/components'),
+}));
+
+describe('', () => {
+ it('Should render the Modal', () => {
+ const mockRootStore = mockStore({
+ traders_hub: {
+ is_wallet_migration_failed: true,
+ },
+ });
+
+ const wrapper = ({ children }: { children: JSX.Element }) => (
+ {children}
+ );
+
+ const { container } = render(, { wrapper });
+
+ expect(container).toBeInTheDocument();
+ });
+
+ it('Should not render the Modal if is_wallet_migration_failed is false', () => {
+ const mockRootStore = mockStore({});
+
+ const wrapper = ({ children }: { children: JSX.Element }) => (
+ {children}
+ );
+
+ const { container } = render(, { wrapper });
+
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/packages/appstore/src/components/modals/wallets-migration-failed/index.ts b/packages/appstore/src/components/modals/wallets-migration-failed/index.ts
new file mode 100644
index 000000000000..1099aa7c3845
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallets-migration-failed/index.ts
@@ -0,0 +1,3 @@
+import WalletsMigrationFailed from './wallets-migration-failed';
+
+export default WalletsMigrationFailed;
diff --git a/packages/appstore/src/components/modals/wallets-migration-failed/wallets-migration-failed.scss b/packages/appstore/src/components/modals/wallets-migration-failed/wallets-migration-failed.scss
new file mode 100644
index 000000000000..f4078494c8a5
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallets-migration-failed/wallets-migration-failed.scss
@@ -0,0 +1,29 @@
+.wallets-migration-failed {
+ padding: 2.4rem !important;
+
+ @include mobile {
+ padding: 1.6rem !important;
+ }
+
+ &__title {
+ margin-bottom: 2.4rem;
+
+ @include mobile {
+ margin-bottom: 1.6rem;
+ }
+ }
+
+ &__footer {
+ padding: 0 2.4rem 2.4rem;
+
+ @include mobile {
+ padding: 0 1.6rem 1.6rem;
+ }
+ }
+
+ &__text {
+ @include mobile {
+ font-size: 1.2rem !important;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/modals/wallets-migration-failed/wallets-migration-failed.tsx b/packages/appstore/src/components/modals/wallets-migration-failed/wallets-migration-failed.tsx
new file mode 100644
index 000000000000..ddca901304c0
--- /dev/null
+++ b/packages/appstore/src/components/modals/wallets-migration-failed/wallets-migration-failed.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { Text, useOnClickOutside, Modal, Button } from '@deriv/components';
+import { useStore, observer } from '@deriv/stores';
+import { Localize } from '@deriv/translations';
+import './wallets-migration-failed.scss';
+
+const WalletsMigrationFailed = observer(() => {
+ const { traders_hub, ui } = useStore();
+ const { is_wallet_migration_failed, setWalletsMigrationFailedPopup } = traders_hub;
+ const { is_mobile } = ui;
+
+ const wallets_migration_failed_ref = React.useRef(null);
+
+ const handleClose = () => {
+ setWalletsMigrationFailedPopup(false);
+ };
+
+ const handLiveChatButtonClick = () => {
+ window.LC_API?.open_chat_window();
+ setWalletsMigrationFailedPopup(false);
+ };
+
+ const validateClickOutside = (e: MouseEvent) => {
+ return is_wallet_migration_failed && !wallets_migration_failed_ref?.current?.contains(e.target as Node);
+ };
+
+ useOnClickOutside(wallets_migration_failed_ref, handleClose, validateClickOutside);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+export default WalletsMigrationFailed;
diff --git a/packages/appstore/src/components/options-multipliers-listing/index.tsx b/packages/appstore/src/components/options-multipliers-listing/index.tsx
index b76afb64a0fc..eeff8ab41626 100644
--- a/packages/appstore/src/components/options-multipliers-listing/index.tsx
+++ b/packages/appstore/src/components/options-multipliers-listing/index.tsx
@@ -1,22 +1,22 @@
import React from 'react';
import { observer } from 'mobx-react-lite';
import { Text, StaticUrl } from '@deriv/components';
+import { ContentFlag } from '@deriv/shared';
+import { useStore } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
import ListingContainer from 'Components/containers/listing-container';
-import { BrandConfig } from 'Constants/platform-config';
-import TradingAppCard from 'Components/containers/trading-app-card';
-import { useStores } from 'Stores/index';
-import { isMobile, ContentFlag } from '@deriv/shared';
import PlatformLoader from 'Components/pre-loader/platform-loader';
+import TradingAppCard from 'Components/containers/trading-app-card';
+import { BrandConfig } from 'Constants/platform-config';
import { getHasDivider } from 'Constants/utils';
-const OptionsAndMultipliersListing = () => {
- const { traders_hub, client, ui } = useStores();
+const OptionsAndMultipliersListing = observer(() => {
+ const { traders_hub, client, ui } = useStore();
const { available_platforms, is_eu_user, is_real, no_MF_account, no_CR_account, is_demo, content_flag } =
traders_hub;
const { is_landing_company_loaded, is_eu, has_maltainvest_account, real_account_creation_unlock_date } = client;
- const { setShouldShowCooldownModal, openRealAccountSignup } = ui;
+ const { setShouldShowCooldownModal, openRealAccountSignup, is_mobile } = ui;
const low_risk_cr_non_eu = content_flag === ContentFlag.LOW_RISK_CR_NON_EU;
@@ -27,15 +27,16 @@ const OptionsAndMultipliersListing = () => {
const cr_demo = content_flag === ContentFlag.CR_DEMO;
const OptionsTitle = () => {
- if ((low_risk_cr_non_eu || high_risk_cr || cr_demo) && !isMobile()) {
+ if (is_mobile) return null;
+ if (low_risk_cr_non_eu || high_risk_cr || cr_demo) {
return (
-
+
);
- } else if ((low_risk_cr_eu || is_eu) && !isMobile()) {
+ } else if (low_risk_cr_eu || is_eu) {
return (
-
+
);
@@ -113,6 +114,6 @@ const OptionsAndMultipliersListing = () => {
)}
);
-};
+});
-export default observer(OptionsAndMultipliersListing);
+export default OptionsAndMultipliersListing;
diff --git a/packages/appstore/src/components/routes/routes-wrapper.tsx b/packages/appstore/src/components/routes/routes-wrapper.tsx
deleted file mode 100644
index 8092cd5b2913..000000000000
--- a/packages/appstore/src/components/routes/routes-wrapper.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as React from 'react';
-import { BrowserRouter as Router } from 'react-router-dom';
-
-const RoutesWrapper: React.FC = ({ has_router, children }) => {
- if (has_router) {
- return {children};
- }
-
- return {children};
-};
-
-type TRoutesWrapperProps = React.PropsWithChildren<{
- has_router: boolean;
-}>;
-
-export default RoutesWrapper;
diff --git a/packages/appstore/src/components/routes/routes.tsx b/packages/appstore/src/components/routes/routes.tsx
index 8d08e7ca9db0..58a844e118be 100644
--- a/packages/appstore/src/components/routes/routes.tsx
+++ b/packages/appstore/src/components/routes/routes.tsx
@@ -1,17 +1,29 @@
import * as React from 'react';
-import { useFeatureFlags } from '@deriv/hooks';
-import { Localize } from '@deriv/translations';
+// import { Loading } from '@deriv/components';
+import { useFeatureFlags /*useWalletsList*/ } from '@deriv/hooks';
+import { observer } from '@deriv/stores';
+import { Localize, localize } from '@deriv/translations';
import Wallets from '@deriv/wallets';
-import getRoutesConfig from 'Constants/routes-config';
-import { useStores } from 'Stores';
-import { TRoute } from 'Types';
-import { observer } from 'mobx-react-lite';
+import Onboarding from 'Modules/onboarding';
+import TradersHub from 'Modules/traders-hub';
+// import { WalletsModule } from 'Modules/wallets';
import { Switch } from 'react-router-dom';
import RouteWithSubroutes from './route-with-sub-routes.jsx';
const Routes: React.FC = observer(() => {
- const { config } = useStores();
- const { is_next_wallet_enabled } = useFeatureFlags();
+ //TODO: Uncomment once useWalletList hook is optimized for production release.
+ const { /*is_wallet_enabled,*/ is_next_wallet_enabled } = useFeatureFlags();
+ // const { has_wallet, isLoading } = useWalletsList();
+ // const should_show_wallets = is_wallet_enabled && has_wallet;
+
+ let content: React.FC = TradersHub;
+ if (is_next_wallet_enabled) {
+ content = Wallets;
+ }
+ // else if (should_show_wallets) {
+ // content = WalletsModule;
+ // }
+ // if (isLoading) return ;
return (
{
}
>
- {getRoutesConfig({
- consumer_routes: config.routes,
- }).map((route: TRoute, idx: number) => {
- // Temporary way to intercept the route to show the Wallets component.
- let updated_route = route;
- if (updated_route.path === '/appstore/traders-hub') {
- updated_route = {
- ...updated_route,
- component: is_next_wallet_enabled ? Wallets : updated_route.component,
- };
- }
-
- return ;
- })}
+ localize("Trader's Hub")}
+ />
+ localize('Onboarding')}
+ />
);
diff --git a/packages/appstore/src/components/transaction-list/index.ts b/packages/appstore/src/components/transaction-list/index.ts
new file mode 100644
index 000000000000..eee3dd112e2f
--- /dev/null
+++ b/packages/appstore/src/components/transaction-list/index.ts
@@ -0,0 +1,3 @@
+import TransactionList from './transaction-list';
+
+export default TransactionList;
diff --git a/packages/appstore/src/components/transaction-list/non-pending-transaction.tsx b/packages/appstore/src/components/transaction-list/non-pending-transaction.tsx
new file mode 100644
index 000000000000..616230a162b7
--- /dev/null
+++ b/packages/appstore/src/components/transaction-list/non-pending-transaction.tsx
@@ -0,0 +1,116 @@
+import React from 'react';
+import { AppLinkedWithWalletIcon, Text, WalletIcon } from '@deriv/components';
+import { useWalletTransactions } from '@deriv/hooks';
+import { useStore } from '@deriv/stores';
+import { Localize } from '@deriv/translations';
+
+type TNonPendingTransaction = {
+ transaction: ReturnType['transactions'][number];
+};
+
+const NonPendingTransaction = ({ transaction }: TNonPendingTransaction) => {
+ const {
+ ui: { is_dark_mode_on, is_mobile },
+ } = useStore();
+
+ const {
+ account_category,
+ account_currency,
+ account_name,
+ account_type,
+ action_type,
+ amount,
+ balance_after = 0,
+ gradient_class,
+ icon,
+ icon_type,
+ } = transaction;
+
+ const formatAmount = (value: number) => value.toLocaleString(undefined, { minimumFractionDigits: 2 });
+
+ const formatActionType = (value: string) => value[0].toUpperCase() + value.substring(1).replace(/_/, ' ');
+
+ const getAppIcon = () => {
+ switch (account_type) {
+ case 'standard':
+ return is_dark_mode_on ? 'IcWalletOptionsDark' : 'IcWalletOptionsLight';
+ //TODO: add proper icon for mt5
+ case 'mt5':
+ return 'IcMt5CfdPlatform';
+ //TODO: add proper icon for dxtrade
+ case 'dxtrade':
+ return '';
+ default:
+ return '';
+ }
+ };
+
+ return (
+
+
+ {account_category === 'trading' ? (
+
+ ) : (
+
+ )}
+
+
+ {formatActionType(action_type)}
+
+
+ {account_name}
+
+
+
+
+ 0 ? 'profit-success' : 'loss-danger'}
+ weight='bold'
+ line_height={is_mobile ? 's' : 'm'}
+ >
+ {(amount > 0 ? '+' : '') + formatAmount(amount)} {account_currency}
+
+
+
+
+
+
+ );
+};
+
+export default NonPendingTransaction;
diff --git a/packages/appstore/src/components/transaction-list/transaction-for-day.tsx b/packages/appstore/src/components/transaction-list/transaction-for-day.tsx
new file mode 100644
index 000000000000..3d28a58231cf
--- /dev/null
+++ b/packages/appstore/src/components/transaction-list/transaction-for-day.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Text } from '@deriv/components';
+import { useWalletTransactions } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import NonPendingTransaction from './non-pending-transaction';
+
+export const TransactionsForOneDay = observer(
+ ({
+ day,
+ transaction_list,
+ }: {
+ day: string;
+ transaction_list: ReturnType['transactions'];
+ }) => {
+ const {
+ client: { loginid },
+ ui: { is_mobile },
+ } = useStore();
+
+ return (
+
+
+ {day}
+
+ {transaction_list.map(transaction => {
+ let display_transaction = transaction;
+ if (
+ transaction?.action_type === 'transfer' &&
+ transaction?.from?.loginid === loginid &&
+ typeof transaction?.amount === 'number'
+ ) {
+ display_transaction = { ...transaction, amount: -transaction.amount };
+ }
+ return ;
+ })}
+
+ );
+ }
+);
diff --git a/packages/appstore/src/components/transaction-list/transaction-list.scss b/packages/appstore/src/components/transaction-list/transaction-list.scss
new file mode 100644
index 000000000000..0d98367bc8f5
--- /dev/null
+++ b/packages/appstore/src/components/transaction-list/transaction-list.scss
@@ -0,0 +1,99 @@
+.transaction-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+
+ &__container {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+ margin: 0 auto;
+ width: 100%;
+ max-width: 800px;
+ }
+
+ &__filter {
+ align-self: end;
+
+ .dc-list {
+ width: 19.5rem;
+ }
+
+ .dc-dropdown {
+ &__container {
+ height: 4rem;
+ width: 19.5rem;
+ }
+
+ &__label {
+ transform: translate(1rem, -1rem) scale(0.75);
+ }
+
+ &__display {
+ height: 4rem;
+ }
+
+ &__display-text {
+ padding-left: 4rem;
+ }
+ }
+
+ .suffix-icon {
+ height: 1.2rem;
+ width: 1.2rem;
+ }
+
+ @include mobile {
+ align-self: auto;
+ margin: 0;
+ width: 100%;
+ max-width: 100%;
+
+ .dc-dropdown__container,
+ .dc-list {
+ width: 100%;
+ }
+ }
+ }
+
+ &__day {
+ display: flex;
+ flex-direction: column;
+
+ &-header {
+ padding: 0.8rem 1.6rem;
+ }
+ }
+
+ &__item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px;
+ box-shadow: inset 0 1px 0 var(--border-normal);
+
+ &__left {
+ display: flex;
+ gap: 0.8rem;
+
+ &__title {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ justify-content: center;
+ gap: 0;
+
+ @include mobile {
+ max-width: 10.4rem;
+ }
+ }
+ }
+
+ &__right {
+ display: flex;
+ flex-direction: column;
+ align-items: end;
+ gap: 0.4rem;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/transaction-list/transaction-list.tsx b/packages/appstore/src/components/transaction-list/transaction-list.tsx
new file mode 100644
index 000000000000..1b24267e68b8
--- /dev/null
+++ b/packages/appstore/src/components/transaction-list/transaction-list.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from 'react';
+import { Dropdown } from '@deriv/components';
+import { useActiveWallet, useWalletTransactions } from '@deriv/hooks';
+import { localize } from '@deriv/translations';
+import { groupTransactionsByDay } from '@deriv/utils';
+import { TransactionsForOneDay } from './transaction-for-day';
+import './transaction-list.scss';
+
+const TransactionList = () => {
+ const wallet = useActiveWallet();
+
+ const filter_options = [
+ {
+ text: localize('All'),
+ value: '',
+ },
+ ...(wallet?.is_virtual
+ ? ([
+ {
+ text: localize('Reset balance'),
+ value: 'reset_balance',
+ },
+ ] as const)
+ : ([
+ {
+ text: localize('Deposit'),
+ value: 'deposit',
+ },
+ {
+ text: localize('Withdrawal'),
+ value: 'withdrawal',
+ },
+ ] as const)),
+ {
+ text: localize('Transfer'),
+ value: 'transfer',
+ },
+ ] as const;
+
+ const [filter, setFilter] = useState('');
+
+ const { transactions } = useWalletTransactions(filter);
+
+ // @ts-expect-error reset_balance is not supported in the API yet
+ const grouped_transactions = groupTransactionsByDay(transactions);
+
+ const onValueChange = (e: { target: { name: string; value: string } }) => {
+ setFilter(e.target.value as typeof filter);
+ };
+
+ return (
+
+
+
+ {Object.entries(grouped_transactions).map(([day, transaction_list]) => (
+ ['transaction_list']
+ }
+ />
+ ))}
+
+
+ );
+};
+
+export default TransactionList;
diff --git a/packages/appstore/src/components/wallet-button/__tests__/wallet-button.spec.tsx b/packages/appstore/src/components/wallet-button/__tests__/wallet-button.spec.tsx
new file mode 100644
index 000000000000..280c3c2756df
--- /dev/null
+++ b/packages/appstore/src/components/wallet-button/__tests__/wallet-button.spec.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import WalletButton from '..';
+
+const mockedRootStore = mockStore({});
+
+describe('', () => {
+ const button = {
+ name: 'Transfer',
+ text: 'Transfer',
+ icon: 'IcAccountTransfer',
+ action: () => {
+ return true;
+ },
+ } as const;
+
+ it('Should render right text', () => {
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('Transfer')).toBeInTheDocument();
+ });
+
+ it('Should render desktop class', () => {
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.childNodes[0]).toHaveClass('wallet-button__desktop-item');
+ expect(container.childNodes[0]).not.toHaveClass('wallet-button__mobile-item');
+ });
+
+ it('Should render mobile class', () => {
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.childNodes[0]).not.toHaveClass('wallet-button__desktop-item');
+ expect(container.childNodes[0]).toHaveClass('wallet-button__mobile-item');
+ });
+
+ it('Should add disabled class', () => {
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.childNodes[0]).not.toHaveClass('wallet-button__mobile-item');
+ expect(container.childNodes[0]).toHaveClass('wallet-button__desktop-item');
+ expect(container.childNodes[0]).toHaveClass('wallet-button__desktop-item-disabled');
+ });
+});
diff --git a/packages/appstore/src/components/wallet-button/index.ts b/packages/appstore/src/components/wallet-button/index.ts
new file mode 100644
index 000000000000..9197d979040f
--- /dev/null
+++ b/packages/appstore/src/components/wallet-button/index.ts
@@ -0,0 +1,3 @@
+import WalletButton from './wallet-button';
+
+export default WalletButton;
diff --git a/packages/appstore/src/components/wallet-button/wallet-button.scss b/packages/appstore/src/components/wallet-button/wallet-button.scss
new file mode 100644
index 000000000000..47478ceb300d
--- /dev/null
+++ b/packages/appstore/src/components/wallet-button/wallet-button.scss
@@ -0,0 +1,79 @@
+.wallet-button {
+ &__mobile-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: pointer;
+ min-width: 5.6rem;
+
+ &-icon {
+ padding: 0.8rem;
+ border: 1px solid var(--border-normal);
+ border-radius: 50%;
+ }
+
+ &-text {
+ margin-top: 0.4rem;
+ }
+ }
+
+ &__desktop-item {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ height: 3.2rem;
+ border-radius: $BORDER_RADIUS * 4;
+ padding: 0.6rem 1.6rem;
+ margin-right: 0.8rem;
+ border: 1px solid var(--border-hover);
+ background-color: var(--prominent);
+
+ &:hover:not(&-disabled) {
+ background-color: var(--button-secondary-hover);
+ }
+
+ &-disabled {
+ cursor: auto;
+ border: 1px solid var(--general-disabled);
+ }
+
+ &-text {
+ margin-left: 0.8rem;
+ }
+
+ &-transition {
+ &-enter {
+ transform: translateX(-1rem);
+ opacity: 0;
+ }
+
+ &-enter-active {
+ transition: all 240ms ease-in-out;
+ transform: translateX(0);
+ position: relative;
+ opacity: 1;
+ }
+
+ &-enter-done {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ &-exit {
+ transform: translateX(0);
+ opacity: 1;
+ }
+
+ &-exit-active {
+ transition: all 240ms ease-in-out;
+ transform: translateX(-1rem);
+ position: relative;
+ opacity: 0;
+ }
+
+ &-exit-done {
+ opacity: 0;
+ }
+ }
+ }
+}
diff --git a/packages/appstore/src/components/wallet-button/wallet-button.tsx b/packages/appstore/src/components/wallet-button/wallet-button.tsx
new file mode 100644
index 000000000000..277025b4dff1
--- /dev/null
+++ b/packages/appstore/src/components/wallet-button/wallet-button.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { CSSTransition } from 'react-transition-group';
+import classNames from 'classnames';
+import { Icon, Text } from '@deriv/components';
+import { getWalletHeaderButtons } from 'Constants/utils';
+import './wallet-button.scss';
+
+type TProps = {
+ button: ReturnType[number];
+ is_desktop_wallet?: boolean;
+ is_disabled?: boolean;
+ is_open?: boolean;
+};
+
+const WalletButton = ({ button, is_desktop_wallet, is_disabled, is_open }: TProps) => {
+ const { name, text, icon, action } = button;
+ return is_desktop_wallet ? (
+
+
+
+
+ {text}
+
+
+
+ ) : (
+
+ );
+};
+
+export default WalletButton;
diff --git a/packages/appstore/src/components/wallet-cards-carousel/__tests__/wallet-cards-carousel.spec.tsx b/packages/appstore/src/components/wallet-cards-carousel/__tests__/wallet-cards-carousel.spec.tsx
new file mode 100644
index 000000000000..f37493775614
--- /dev/null
+++ b/packages/appstore/src/components/wallet-cards-carousel/__tests__/wallet-cards-carousel.spec.tsx
@@ -0,0 +1,165 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { APIProvider, useFetch } from '@deriv/api';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import WalletCardsCarousel from '..';
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useFetch: jest.fn((name: string) => {
+ if (name === 'authorize') {
+ return {
+ data: {
+ authorize: {
+ account_list: [
+ {
+ account_category: 'wallet',
+ currency: 'USD',
+ is_virtual: 0,
+ loginid: 'CRW10001',
+ },
+ {
+ account_category: 'trading',
+ currency: 'USD',
+ is_virtual: 0,
+ loginid: 'CRW10002',
+ },
+ {
+ account_category: 'wallet',
+ currency: 'UST',
+ is_virtual: 0,
+ loginid: 'CRW10003',
+ },
+ {
+ account_category: 'wallet',
+ currency: 'BTC',
+ is_virtual: 1,
+ loginid: 'VRW10001',
+ },
+ {
+ account_category: 'wallet',
+ currency: 'AUD',
+ is_virtual: 0,
+ loginid: 'CRW10004',
+ },
+ {
+ account_category: 'wallet',
+ currency: 'ETH',
+ is_virtual: 0,
+ loginid: 'CRW10005',
+ },
+ ],
+ },
+ },
+ };
+ } else if (name === 'balance') {
+ return {
+ data: {
+ balance: {
+ accounts: {
+ CRW909900: {
+ balance: 0,
+ },
+ },
+ },
+ },
+ };
+ } else if (name === 'website_status') {
+ return {
+ data: {
+ website_status: {
+ currencies_config: {
+ AUD: { type: 'fiat' },
+ BTC: { type: 'crypto' },
+ ETH: { type: 'crypto' },
+ UST: { type: 'crypto' },
+ USD: { type: 'fiat' },
+ },
+ },
+ },
+ };
+ } else if (name === 'crypto_config') {
+ return {
+ data: {
+ crypto_config: {
+ currencies_config: {
+ BTC: {},
+ },
+ },
+ },
+ };
+ }
+
+ return undefined;
+ }),
+}));
+
+jest.mock('./../cards-slider-swiper', () => jest.fn(() => slider
));
+const mockUseFetch = useFetch as jest.MockedFunction>;
+
+describe('', () => {
+ const wrapper = (mock: ReturnType) => {
+ const Component = ({ children }: { children: JSX.Element }) => (
+
+ {children}
+
+ );
+ return Component;
+ };
+ it('Should render slider', () => {
+ const mock = mockStore({ client: { accounts: { CRW909900: { token: '12345' } }, loginid: 'CRW909900' } });
+
+ render(, { wrapper: wrapper(mock) });
+ const slider = screen.queryByText('slider');
+
+ expect(slider).toBeInTheDocument();
+ });
+
+ it('Should render buttons for REAL', () => {
+ const mock = mockStore({ client: { accounts: { CRW909900: { token: '12345' } }, loginid: 'CRW909900' } });
+
+ render(, { wrapper: wrapper(mock) });
+
+ const btn1 = screen.getByRole('button', { name: /Deposit/i });
+ const btn2 = screen.getByRole('button', { name: /Withdraw/i });
+ const btn3 = screen.getByRole('button', { name: /Transfer/i });
+ const btn4 = screen.getByRole('button', { name: /Transactions/i });
+
+ expect(btn1).toBeInTheDocument();
+ expect(btn2).toBeInTheDocument();
+ expect(btn3).toBeInTheDocument();
+ expect(btn4).toBeInTheDocument();
+ });
+
+ it('Should render buttons for DEMO', () => {
+ const mock = mockStore({ client: { accounts: { VRW10001: { token: '12345' } }, loginid: 'VRW10001' } });
+
+ mockUseFetch.mockReturnValue({
+ data: {
+ authorize: {
+ account_list: [
+ {
+ account_category: 'wallet',
+ account_type: 'doughflow',
+ currency: 'USD',
+ is_virtual: 1,
+ loginid: 'VRW10001',
+ },
+ ],
+ loginid: 'VRW10001',
+ },
+ },
+ } as unknown as ReturnType);
+
+ render(, { wrapper: wrapper(mock) });
+
+ const btn1 = screen.getByRole('button', { name: /Transfer/i });
+ const btn2 = screen.getByRole('button', { name: /Transactions/i });
+ const btn3 = screen.getByRole('button', { name: /Reset balance/i });
+
+ expect(btn1).toBeInTheDocument();
+ expect(btn2).toBeInTheDocument();
+ expect(btn3).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/wallet-cards-carousel/cards-slider-swiper.tsx b/packages/appstore/src/components/wallet-cards-carousel/cards-slider-swiper.tsx
new file mode 100644
index 000000000000..dfa33194257d
--- /dev/null
+++ b/packages/appstore/src/components/wallet-cards-carousel/cards-slider-swiper.tsx
@@ -0,0 +1,86 @@
+import React, { useEffect, useState } from 'react';
+import useEmblaCarousel from 'embla-carousel-react';
+import { WalletCard, ProgressBarTracker } from '@deriv/components';
+import { useWalletsList } from '@deriv/hooks';
+import { formatMoney } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
+import { getAccountName } from 'Constants/utils';
+import { TWalletAccount } from 'Types';
+import './wallet-cards-carousel.scss';
+
+const CardsSliderSwiper = observer(() => {
+ const { client } = useStore();
+ const { switchAccount } = client;
+ const { data } = useWalletsList();
+
+ const active_wallet_index = data.findIndex(item => item?.is_selected) || 0;
+
+ const [active_index, setActiveIndex] = useState(active_wallet_index);
+ const [emblaRef, emblaApi] = useEmblaCarousel({ skipSnaps: true, containScroll: false });
+
+ const steps = data.map((_, idx) => idx.toString());
+
+ useEffect(() => {
+ emblaApi?.on('select', () => {
+ const index = emblaApi?.selectedScrollSnap() || 0;
+ setActiveIndex(index + 1);
+ });
+ }, [emblaApi]);
+
+ useEffect(() => {
+ emblaApi?.scrollTo(active_index - 1);
+ }, [active_index, emblaApi]);
+
+ useEffect(() => {
+ const timeout_id = setTimeout(() => {
+ if (!data[active_index - 1]?.is_selected) switchAccount(data[active_index - 1]?.loginid);
+ }, 1000);
+
+ return () => clearTimeout(timeout_id);
+ }, [active_index, data, switchAccount]);
+
+ useEffect(() => {
+ setActiveIndex(active_wallet_index + 1);
+ }, [active_wallet_index]);
+
+ const slider = React.useMemo(
+ () =>
+ data?.map((item: TWalletAccount) => {
+ const { loginid, icon, currency_config, balance, currency, landing_company_name, gradient_card_class } =
+ item;
+ return (
+
+
+
+ );
+ }),
+ [data?.length]
+ );
+
+ return (
+
+
+
+
+ );
+});
+
+export default CardsSliderSwiper;
diff --git a/packages/appstore/src/components/wallet-cards-carousel/index.ts b/packages/appstore/src/components/wallet-cards-carousel/index.ts
new file mode 100644
index 000000000000..7a4b6f3d0f73
--- /dev/null
+++ b/packages/appstore/src/components/wallet-cards-carousel/index.ts
@@ -0,0 +1,3 @@
+import WalletCardsCarousel from './wallet-cards-carousel';
+
+export default WalletCardsCarousel;
diff --git a/packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.scss b/packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.scss
new file mode 100644
index 000000000000..9279aafa264b
--- /dev/null
+++ b/packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.scss
@@ -0,0 +1,36 @@
+.wallet-cards-carousel {
+ margin: -1.6rem -1.6rem -1.4rem;
+ padding: 2.4rem 0 1.6rem;
+
+ &__viewport {
+ overflow: hidden;
+ }
+ &__container {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 2.4rem;
+ }
+ &__pagination {
+ margin-block: 1.6rem;
+ }
+
+ &__buttons {
+ display: flex;
+ justify-content: center;
+ gap: 0.8rem;
+ }
+}
+
+.wallet-carousel-content-container {
+ display: flex;
+ padding: 1.6rem;
+ flex-direction: column;
+ align-items: center;
+ background-color: var(--general-main-1);
+
+ &-demo {
+ background-color: var(--wallet-demo-bg-color);
+ }
+}
diff --git a/packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.tsx b/packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.tsx
new file mode 100644
index 000000000000..5473bdf3bf45
--- /dev/null
+++ b/packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { useActiveWallet } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import WalletButton from 'Components/wallet-button';
+import { getWalletHeaderButtons } from 'Constants/utils';
+import CardsSliderSwiper from './cards-slider-swiper';
+import './wallet-cards-carousel.scss';
+
+const WalletCardsCarousel = observer(() => {
+ const { ui, traders_hub } = useStore();
+ const { setIsWalletModalVisible } = ui;
+ const { setWalletModalActiveWalletID, setWalletModalActiveTab } = traders_hub;
+ const active_wallet = useActiveWallet();
+
+ const wallet_buttons = getWalletHeaderButtons(active_wallet?.is_demo || false);
+
+ return (
+
+
+
+ {wallet_buttons.map(button => {
+ button.action = () => {
+ setWalletModalActiveTab(button.name);
+ setIsWalletModalVisible(true);
+ setWalletModalActiveWalletID(active_wallet?.loginid);
+ };
+
+ return ;
+ })}
+
+
+ );
+});
+
+export default WalletCardsCarousel;
diff --git a/packages/appstore/src/components/wallet-content/__tests__/wallet-content-divider.spec.tsx b/packages/appstore/src/components/wallet-content/__tests__/wallet-content-divider.spec.tsx
new file mode 100644
index 000000000000..bddb0ec95bcc
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/__tests__/wallet-content-divider.spec.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import WalletContentDivider from '../wallet-content-divider';
+
+describe('', () => {
+ it('Check classname for NOT demo', () => {
+ const { container } = render();
+
+ expect(container.childNodes[0]).toHaveClass('wallet-content__divider');
+ expect(container.childNodes[0]).not.toHaveClass('wallet-content__divider-demo');
+ });
+
+ it('Check classname for demo', () => {
+ const { container } = render();
+
+ expect(container.childNodes[0]).toHaveClass('wallet-content__divider');
+ expect(container.childNodes[0]).toHaveClass('wallet-content__divider-demo');
+ });
+});
diff --git a/packages/appstore/src/components/wallet-content/__tests__/wallet-content.spec.tsx b/packages/appstore/src/components/wallet-content/__tests__/wallet-content.spec.tsx
new file mode 100644
index 000000000000..925c2bd480c1
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/__tests__/wallet-content.spec.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import WalletContent from '../wallet-content';
+
+const mockedRootStore = mockStore({
+ modules: {
+ cfd: {
+ toggleCompareAccountsModal: jest.fn(),
+ },
+ },
+});
+
+jest.mock('./../../containers/currency-switcher-container', () => jest.fn(({ children }) => {children}
));
+
+jest.mock('@deriv/hooks', () => ({
+ ...jest.requireActual('@deriv/hooks'),
+ useActiveWallet: jest.fn(),
+}));
+
+describe('', () => {
+ it('Check class', () => {
+ render(
+
+
+
+ );
+
+ const wrapper = screen.queryByTestId('dt_wallet-content');
+ expect(wrapper).toHaveClass('wallet-content');
+ expect(wrapper).not.toHaveClass('wallet-content__demo');
+ });
+
+ it('Check class for demo', () => {
+ render(
+
+
+
+ );
+
+ const wrapper = screen.queryByTestId('dt_wallet-content');
+ expect(wrapper).toHaveClass('wallet-content');
+ expect(wrapper).toHaveClass('wallet-content__demo');
+ });
+
+ it('Check there is NOT disclaimer for demo', () => {
+ render(
+
+
+
+ );
+
+ const disclaimer = screen.queryByTestId('dt_disclaimer_wrapper');
+
+ expect(disclaimer).not.toBeInTheDocument();
+ });
+
+ it('Check there is NOT disclaimer for Non-EU', () => {
+ render(
+
+
+
+ );
+
+ const disclaimer = screen.queryByTestId('dt_disclaimer_wrapper');
+
+ expect(disclaimer).not.toBeInTheDocument();
+ });
+
+ it('Check there is disclaimer for EU and not demo', () => {
+ render(
+
+
+
+ );
+
+ const disclaimer = screen.queryByTestId('dt_disclaimer_wrapper');
+
+ expect(disclaimer).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/wallet-content/__tests__/wallet-transfer-block.spec.tsx b/packages/appstore/src/components/wallet-content/__tests__/wallet-transfer-block.spec.tsx
new file mode 100644
index 000000000000..71b3d47ae566
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/__tests__/wallet-transfer-block.spec.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import WalletTransferBlock from '../wallet-transfer-block';
+import { TWalletAccount } from 'Types';
+import { StoreProvider, mockStore } from '@deriv/stores';
+
+const wallet_account: TWalletAccount = {
+ name: 'USD',
+ currency: 'USD',
+ icon: '',
+ balance: 10415.24,
+ icon_type: 'fiat',
+ landing_company_name: 'svg',
+ is_disabled: false,
+ is_virtual: false,
+ loginid: 'CRW10001',
+};
+
+jest.mock('./../../containers/currency-switcher-container', () => jest.fn(({ children }) => {children}
));
+
+const mockedRootStore = mockStore({
+ modules: {
+ cfd: {
+ toggleCompareAccountsModal: jest.fn(),
+ },
+ },
+});
+
+describe('', () => {
+ it('Check balance', () => {
+ render(
+
+
+
+ );
+ const { currency } = wallet_account;
+
+ const balance_title = screen.getByText(`10,415.24 ${currency}`);
+
+ expect(balance_title).toBeInTheDocument();
+ });
+
+ it('Check loginid', () => {
+ render(
+
+
+
+ );
+ const { loginid } = wallet_account;
+
+ const loginid_title = screen.getByText(String(loginid));
+
+ expect(loginid_title).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/wallet-content/index.ts b/packages/appstore/src/components/wallet-content/index.ts
new file mode 100644
index 000000000000..fee1bf581d8b
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/index.ts
@@ -0,0 +1,3 @@
+import WalletContent from './wallet-content';
+
+export default WalletContent;
diff --git a/packages/appstore/src/components/wallet-content/wallet-cfds-listing.tsx b/packages/appstore/src/components/wallet-content/wallet-cfds-listing.tsx
new file mode 100644
index 000000000000..bfb0a014da40
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/wallet-cfds-listing.tsx
@@ -0,0 +1,253 @@
+import React from 'react';
+import { Text, StaticUrl, Button } from '@deriv/components';
+import { useActiveWallet, useCFDCanGetMoreMT5Accounts } from '@deriv/hooks';
+import { formatMoney, isCryptocurrency } from '@deriv/shared';
+import { localize, Localize } from '@deriv/translations';
+import ListingContainer from 'Components/containers/listing-container';
+import TradingAppCard from 'Components/containers/trading-app-card';
+import PlatformLoader from 'Components/pre-loader/platform-loader';
+import { getHasDivider } from 'Constants/utils';
+import { useStore, observer } from '@deriv/stores';
+import GetMoreAccounts from 'Components/get-more-accounts';
+import { TDetailsOfEachMT5Loginid } from 'Types';
+import './wallet-content.scss';
+
+type TProps = {
+ fiat_wallet_currency?: string;
+};
+
+const CryptoCFDs = observer(({ fiat_wallet_currency }: TProps) => {
+ const { traders_hub, ui } = useStore();
+ const { setWalletModalActiveWalletID, setWalletModalActiveTab } = traders_hub;
+
+ const { is_mobile, setIsWalletModalVisible } = ui;
+
+ const wallet_account = useActiveWallet();
+ if (!wallet_account) return null;
+
+ return (
+
+
+
+
+
+
+ );
+});
+
+const FiatCFDs = observer(() => {
+ const { traders_hub } = useStore();
+ const {
+ selected_region,
+ getExistingAccounts,
+ selected_account_type,
+ available_dxtrade_accounts,
+ combined_cfd_mt5_accounts,
+ toggleAccountTypeModalVisibility,
+ } = traders_hub;
+
+ const can_get_more_cfd_mt5_accounts = useCFDCanGetMoreMT5Accounts();
+
+ const wallet_account = useActiveWallet();
+ if (!wallet_account) return null;
+
+ const getMT5AccountAuthStatus = (current_acc_status: string) => {
+ if (current_acc_status === 'proof_failed') {
+ return 'failed';
+ }
+ if (current_acc_status === 'verification_pending') {
+ return 'pending';
+ }
+ return null;
+ };
+
+ return (
+
+
+
+
+
+
+ {combined_cfd_mt5_accounts.map((existing_account, index) => {
+ const {
+ action_type,
+ description,
+ icon,
+ key,
+ landing_company_short,
+ market_type,
+ name,
+ platform,
+ status,
+ sub_title,
+ } = existing_account;
+ const list_size = combined_cfd_mt5_accounts.length;
+ const mt5_account_status = status ? getMT5AccountAuthStatus(status) : null;
+ return (
+
+ );
+ })}
+ {can_get_more_cfd_mt5_accounts && (
+
+ )}
+ {available_dxtrade_accounts?.length > 0 && (
+
+
+
+
+
+ )}
+ {available_dxtrade_accounts?.map(account => {
+ const existing_accounts = getExistingAccounts(account.platform ?? '', account.market_type ?? '');
+ const has_existing_accounts = existing_accounts.length > 0;
+ return has_existing_accounts ? (
+ existing_accounts.map((existing_account: TDetailsOfEachMT5Loginid) => (
+
+ ))
+ ) : (
+
+ );
+ })}
+
+ );
+});
+
+const WalletCFDsListing = observer(({ fiat_wallet_currency = 'USD' }: TProps) => {
+ const {
+ client,
+ modules: { cfd },
+ ui,
+ } = useStore();
+
+ const { toggleCompareAccountsModal } = cfd;
+ const { is_landing_company_loaded, is_logging_in, is_switching } = client;
+ const { is_mobile } = ui;
+
+ const wallet_account = useActiveWallet();
+
+ if (!wallet_account || !is_landing_company_loaded || is_switching || is_logging_in)
+ return (
+
+ );
+
+ const { currency, landing_company_name, is_virtual } = wallet_account;
+ const accounts_sub_text =
+ landing_company_name === 'svg' || is_virtual ? (
+
+ ) : (
+
+ );
+
+ const is_fiat = !isCryptocurrency(currency) && currency !== 'USDT';
+
+ return (
+
+
+
+
+
+
+ {accounts_sub_text}
+
+
+
+ )
+ }
+ description={
+
+ Learn more0>'
+ }
+ components={[]}
+ />
+
+ }
+ is_outside_grid_container={!is_fiat}
+ >
+ {is_mobile && (
+
+
+ {accounts_sub_text}
+
+
+ )}
+ {is_fiat ?
:
}
+
+ );
+});
+
+export default WalletCFDsListing;
diff --git a/packages/appstore/src/components/wallet-content/wallet-content-divider.tsx b/packages/appstore/src/components/wallet-content/wallet-content-divider.tsx
new file mode 100644
index 000000000000..442c7c4ad486
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/wallet-content-divider.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import classNames from 'classnames';
+
+const WalletContentDivider = ({ is_demo_divider }: { is_demo_divider?: boolean }) => (
+
+);
+
+export default WalletContentDivider;
diff --git a/packages/appstore/src/components/wallet-content/wallet-content.scss b/packages/appstore/src/components/wallet-content/wallet-content.scss
new file mode 100644
index 000000000000..dca6d6f6931d
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/wallet-content.scss
@@ -0,0 +1,75 @@
+.wallet-content {
+ &__loader {
+ padding: 2.4rem;
+
+ @include mobile {
+ padding: 1.6rem 0;
+ }
+ }
+
+ &__divider {
+ border: 1px solid var(--general-section-1);
+ margin-inline: 2.4rem;
+
+ &-demo {
+ border-color: var(--wallet-demo-divider-color);
+ }
+ }
+
+ &__border-reset {
+ border: 0;
+ }
+
+ &__disclaimer {
+ width: 100%;
+ height: 7rem;
+ display: flex;
+ align-items: center;
+ backface-visibility: hidden;
+ background-color: var(--wallet-eu-disclaimer);
+ border-radius: 0 0 $BORDER_RADIUS * 4 $BORDER_RADIUS * 4;
+
+ @include mobile {
+ height: 8rem;
+ border: 1px solid var(--wallet-eu-disclaimer);
+ }
+
+ &-text {
+ padding: 0 4rem;
+
+ @include mobile {
+ padding: 0 1.5rem;
+ }
+ }
+ }
+
+ &__cfd {
+ & .listing-container__content {
+ padding-block-start: 0;
+ }
+
+ & .cfd-accounts__compare-table-title {
+ padding-left: 0.8rem;
+ }
+
+ &-crypto {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 2.4rem;
+
+ @include mobile {
+ padding: 0;
+ }
+
+ &-title {
+ padding: 2.4rem;
+
+ @include mobile {
+ padding: 2.4rem 1.6rem;
+ text-align: center;
+ }
+ }
+ }
+ }
+}
diff --git a/packages/appstore/src/components/wallet-content/wallet-content.tsx b/packages/appstore/src/components/wallet-content/wallet-content.tsx
new file mode 100644
index 000000000000..195b87c978b9
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/wallet-content.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import classNames from 'classnames';
+import ContentDivider from './wallet-content-divider';
+import WalletCfdsListing from './wallet-cfds-listing';
+import WalletOptionsAndMultipliersListing from './wallet-option-multipliers-listing';
+import EUDisclaimer from 'Components/eu-disclaimer';
+import './wallet-content.scss';
+
+type TProps = {
+ is_demo: boolean;
+ is_malta_wallet: boolean;
+};
+
+const WalletContent = ({ is_demo, is_malta_wallet }: TProps) => {
+ return (
+
+
+
+
+
+ {is_malta_wallet && !is_demo && (
+
+ )}
+
+ );
+};
+
+export default WalletContent;
diff --git a/packages/appstore/src/components/wallet-content/wallet-option-multipliers-listing.tsx b/packages/appstore/src/components/wallet-content/wallet-option-multipliers-listing.tsx
new file mode 100644
index 000000000000..a3f39f67a5cb
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/wallet-option-multipliers-listing.tsx
@@ -0,0 +1,131 @@
+import React from 'react';
+import { Text, StaticUrl } from '@deriv/components';
+import { Localize, localize } from '@deriv/translations';
+import ListingContainer from 'Components/containers/listing-container';
+import TradingAppCard from 'Components/containers/trading-app-card';
+import PlatformLoader from 'Components/pre-loader/platform-loader';
+import { getHasDivider } from 'Constants/utils';
+import { Jurisdiction } from '@deriv/shared';
+import { useStore, observer } from '@deriv/stores';
+import { useActiveWallet } from '@deriv/hooks';
+import './wallet-content.scss';
+
+type TProps = {
+ landing_company_name: string | undefined;
+};
+
+const OptionsTitle = observer(({ landing_company_name }: TProps) => {
+ const {
+ ui: { is_mobile },
+ } = useStore();
+
+ const is_svg_wallet = landing_company_name === 'svg';
+
+ if (is_svg_wallet && !is_mobile) {
+ return (
+
+
+
+ );
+ } else if (!is_svg_wallet && !is_mobile) {
+ return (
+
+
+
+ );
+ }
+ return null;
+});
+
+const ListingContainerDescription = ({ landing_company_name }: TProps) =>
+ landing_company_name === 'svg' ? (
+
+ ,
+ ,
+ ]}
+ />
+
+ ) : (
+
+ ]}
+ />
+
+ );
+
+const WalletOptionsAndMultipliersListing = observer(() => {
+ const { traders_hub, client, ui } = useStore();
+ const { setShouldShowCooldownModal, openRealAccountSignup } = ui;
+ const {
+ is_landing_company_loaded,
+ has_maltainvest_account,
+ real_account_creation_unlock_date,
+ is_logging_in,
+ is_switching,
+ } = client;
+ const { available_platforms, is_eu_user, is_real, no_MF_account, no_CR_account, is_demo } = traders_hub;
+
+ const wallet_account = useActiveWallet();
+
+ if (!wallet_account || is_switching || is_logging_in || !is_landing_company_loaded) {
+ return (
+
+ );
+ }
+
+ const platforms_action_type =
+ is_demo || (!no_CR_account && !is_eu_user) || (has_maltainvest_account && is_eu_user) ? 'trade' : 'none';
+
+ const derivAccountAction = () => {
+ if (no_MF_account) {
+ if (real_account_creation_unlock_date) {
+ setShouldShowCooldownModal(true);
+ } else {
+ openRealAccountSignup(Jurisdiction.MALTA_INVEST);
+ }
+ } else {
+ openRealAccountSignup(Jurisdiction.SVG);
+ }
+ };
+
+ return (
+
}
+ description={
}
+ is_deriv_platform
+ >
+ {is_real && (no_CR_account || no_MF_account) && (
+
+
+
+ )}
+ {available_platforms.map((available_platform, index) => (
+
+ ))}
+
+ );
+});
+
+export default WalletOptionsAndMultipliersListing;
diff --git a/packages/appstore/src/components/wallet-content/wallet-transfer-block.tsx b/packages/appstore/src/components/wallet-content/wallet-transfer-block.tsx
new file mode 100644
index 000000000000..b2e04796583d
--- /dev/null
+++ b/packages/appstore/src/components/wallet-content/wallet-transfer-block.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import { Button, Text } from '@deriv/components';
+import { formatMoney } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
+import { Localize } from '@deriv/translations';
+import CurrencySwitcherContainer from 'Components/containers/currency-switcher-container';
+import { TWalletAccount } from 'Types';
+
+type TProps = {
+ wallet_account: TWalletAccount;
+};
+
+const WalletTransferBlock = observer(({ wallet_account }: TProps) => {
+ const { traders_hub, ui } = useStore();
+ const { setIsWalletModalVisible } = ui;
+ const { setWalletModalActiveWalletID, setWalletModalActiveTab } = traders_hub;
+
+ const { currency, balance, loginid } = wallet_account;
+
+ return (
+
{
+ setWalletModalActiveTab('Transfer');
+ setIsWalletModalVisible(true);
+ setWalletModalActiveWalletID(loginid);
+ }}
+ secondary
+ className='currency-switcher__button'
+ >
+
+
+ }
+ has_interaction
+ show_dropdown={false}
+ >
+
+
+ {formatMoney(currency, balance, true)} {currency}
+
+
+ {loginid}
+
+
+
+ );
+});
+export default WalletTransferBlock;
diff --git a/packages/appstore/src/components/wallet-deposit/index.ts b/packages/appstore/src/components/wallet-deposit/index.ts
new file mode 100644
index 000000000000..7a81ec16480e
--- /dev/null
+++ b/packages/appstore/src/components/wallet-deposit/index.ts
@@ -0,0 +1,3 @@
+import WalletDeposit from './wallet-deposit';
+
+export default WalletDeposit;
diff --git a/packages/appstore/src/components/wallet-deposit/wallet-deposit.scss b/packages/appstore/src/components/wallet-deposit/wallet-deposit.scss
new file mode 100644
index 000000000000..2daf382ce975
--- /dev/null
+++ b/packages/appstore/src/components/wallet-deposit/wallet-deposit.scss
@@ -0,0 +1,7 @@
+.wallet-deposit {
+ &__fiat-container {
+ display: flex;
+ max-width: 58.8rem;
+ margin: 0 auto;
+ }
+}
diff --git a/packages/appstore/src/components/wallet-deposit/wallet-deposit.tsx b/packages/appstore/src/components/wallet-deposit/wallet-deposit.tsx
new file mode 100644
index 000000000000..4e26439f5c3f
--- /dev/null
+++ b/packages/appstore/src/components/wallet-deposit/wallet-deposit.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { useCurrencyConfig } from '@deriv/hooks';
+import { useStore, observer } from '@deriv/stores';
+import { Div100vhContainer } from '@deriv/components';
+import DepositFiatIframe from '@deriv/cashier/src/modules/deposit-fiat/components/deposit-fiat-iframe/deposit-fiat-iframe';
+import './wallet-deposit.scss';
+
+const WalletDeposit = observer(() => {
+ const { client, ui } = useStore();
+ const { is_mobile } = ui;
+ const { currency, loginid } = client;
+
+ const { getConfig } = useCurrencyConfig();
+ const currency_config = getConfig(currency);
+ const is_crypto = currency_config?.is_crypto;
+
+ //TODO: remove when selected wallet will be provided to WalletDeposit props
+ const real_fiat_wallet = loginid?.startsWith('CRW') && !is_crypto;
+
+ return real_fiat_wallet ? (
+
+
+
+ ) : (
+
Deposit Development Is In Progress
+ );
+});
+
+export default WalletDeposit;
diff --git a/packages/appstore/src/components/wallet-header/__tests__/wallet-header.spec.tsx b/packages/appstore/src/components/wallet-header/__tests__/wallet-header.spec.tsx
new file mode 100644
index 000000000000..b506d6c39f44
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/__tests__/wallet-header.spec.tsx
@@ -0,0 +1,326 @@
+import React from 'react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { getStatusBadgeConfig } from '@deriv/account';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import { TWalletAccount } from 'Types';
+import WalletHeader from '..';
+
+const mockedRootStore = mockStore({});
+
+jest.mock('@deriv/account', () => ({
+ ...jest.requireActual('@deriv/account'),
+ getStatusBadgeConfig: jest.fn(() => ({ icon: '', text: '' })),
+}));
+
+jest.mock('@deriv/hooks', () => ({
+ ...jest.requireActual('@deriv/hooks'),
+ useWalletModalActionHandler: jest.fn(() => ({ setWalletModalActiveTabIndex: jest.fn(), handleAction: jest.fn() })),
+}));
+
+describe('
', () => {
+ const default_mocked_props: TWalletAccount = {
+ is_demo: false,
+ currency: 'USD',
+ landing_company_name: 'svg',
+ balance: 10000,
+ loginid: 'CRW123123',
+ is_malta_wallet: false,
+ is_selected: true,
+ gradient_header_class: 'wallet-header__usd-bg',
+ gradient_card_class: 'wallet-card__usd-bg',
+ wallet_currency_type: '',
+ currency_config: undefined,
+ icon: '',
+ };
+
+ describe('Check currency card', () => {
+ it('Should render right currency card for DEMO', () => {
+ const mocked_props = { ...default_mocked_props, is_demo: true };
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId(`dt_demo`)).toBeInTheDocument();
+ });
+
+ it('Should render right currency card for REAL SVG fiat', () => {
+ const mocked_props = { ...default_mocked_props, currency: 'AUD' };
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId(`dt_${mocked_props.currency.toLowerCase()}`)).toBeInTheDocument();
+ });
+
+ it('Should render right currency card for REAL SVG crypto', () => {
+ const mocked_props = { ...default_mocked_props, currency: 'ETH' };
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId(`dt_${mocked_props.currency.toLowerCase()}`)).toBeInTheDocument();
+ });
+
+ it('Should render right currency card for REAL MALTA fiat', () => {
+ const mocked_props = { ...default_mocked_props, currency: 'ETH', landing_company_name: 'malta' };
+ render(
+
+
+
+ );
+
+ expect(screen.queryByTestId(`dt_${mocked_props.currency.toLowerCase()}`)).toBeInTheDocument();
+ });
+ });
+
+ describe('Check balance', () => {
+ it('Should render right balance with balance as props', () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 2345.56,
+ };
+ render(
+
+
+
+ );
+
+ expect(screen.getByText('2,345.56 EUR')).toBeInTheDocument();
+ });
+
+ it('Should render balance === 0.00', () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+ render(
+
+
+
+ );
+
+ expect(screen.queryByText(`0.00 ${mocked_props.currency}`)).toBeInTheDocument();
+ });
+
+ it('Should render badge Pending verification', () => {
+ getStatusBadgeConfig.mockReturnValue({ icon: '', text: 'Pending verification' });
+
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'MFW1231',
+ },
+ traders_hub: { multipliers_account_status: 'pending', is_eu_user: true },
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByText(/Pending verification/i)).toBeInTheDocument();
+ expect(screen.queryByText(/balance/i)).not.toBeInTheDocument();
+ });
+
+ it('Should render badge Verification failed', () => {
+ getStatusBadgeConfig.mockReturnValue({ icon: '', text: 'Verification failed' });
+
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'MFW1231',
+ },
+ traders_hub: { multipliers_account_status: 'failed', is_eu_user: true },
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByText(/Verification failed/i)).toBeInTheDocument();
+ expect(screen.queryByText(/balance/i)).not.toBeInTheDocument();
+ });
+
+ it('Should render badge Need verification', () => {
+ getStatusBadgeConfig.mockReturnValue({ icon: '', text: 'Need verification' });
+
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'MFW1231',
+ },
+ traders_hub: { multipliers_account_status: 'need_verification', is_eu_user: true },
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByText(/Need verification/i)).toBeInTheDocument();
+ expect(screen.queryByText(/balance/i)).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Check buttons', () => {
+ it('Buttons collapsed', () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ balance: 0,
+ is_selected: false,
+ is_demo: true,
+ };
+
+ render(
+
+
+
+ );
+
+ expect(screen.queryByRole('button', { name: /Transfer/i })).not.toBeInTheDocument();
+ });
+
+ it('Buttons uncollapsed', () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ balance: 0,
+ is_selected: true,
+ is_demo: true,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'CRW1231',
+ },
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole('button', { name: /Transfer/i })).toBeInTheDocument();
+ });
+
+ it('Arrow button click and switchAccount should be called', async () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ balance: 0,
+ is_selected: false,
+ is_demo: true,
+ loginid: 'CRW1231',
+ };
+
+ render(
+
+
+
+ );
+
+ const arrow_btn = screen.getByTestId('dt_arrow');
+ userEvent.click(arrow_btn);
+
+ await waitFor(() => {
+ expect(mockedRootStore.client.switchAccount).toBeCalledTimes(1);
+ });
+ });
+
+ it('Check buttons for demo', () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ balance: 0,
+ currency: 'EUR',
+ is_demo: true,
+ loginid: 'VRW123123',
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'VRW123123',
+ },
+ });
+
+ render(
+
+
+
+ );
+
+ const transfer_btn = screen.getByRole('button', { name: /Transfer/i });
+ const transactions_btn = screen.getByRole('button', { name: /Transactions/i });
+ const reset_btn = screen.getByRole('button', { name: /Reset balance/i });
+
+ expect(transfer_btn).toBeInTheDocument();
+ expect(transactions_btn).toBeInTheDocument();
+ expect(reset_btn).toBeInTheDocument();
+ });
+
+ it('Check buttons for real', () => {
+ const mocked_props = {
+ ...default_mocked_props,
+ balance: 1230,
+ currency: 'EUR',
+ loginid: 'CRW123123',
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'CRW123123',
+ },
+ });
+
+ render(
+
+
+
+ );
+
+ const deposit_btn = screen.getByRole('button', { name: /Deposit/i });
+ const withdraw_btn = screen.getByRole('button', { name: /Withdraw/i });
+ const transfer_btn = screen.getByRole('button', { name: /Transfer/i });
+ const transactions_btn = screen.getByRole('button', { name: /Transactions/i });
+
+ expect(deposit_btn).toBeInTheDocument();
+ expect(withdraw_btn).toBeInTheDocument();
+ expect(transfer_btn).toBeInTheDocument();
+ expect(transactions_btn).toBeInTheDocument();
+ });
+ });
+});
diff --git a/packages/appstore/src/components/wallet-header/index.ts b/packages/appstore/src/components/wallet-header/index.ts
new file mode 100644
index 000000000000..ed176c4b242f
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/index.ts
@@ -0,0 +1,3 @@
+import WalletHeader from './wallet-header';
+
+export default WalletHeader;
diff --git a/packages/appstore/src/components/wallet-header/wallet-currency-card.tsx b/packages/appstore/src/components/wallet-header/wallet-currency-card.tsx
new file mode 100644
index 000000000000..967614865c52
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-currency-card.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+import { WalletIcon } from '@deriv/components';
+import { TWalletAccount } from 'Types';
+
+type TWalletCurrencyCard = Pick
& {
+ gradient_class?: string;
+ icon_type?: string;
+};
+
+const WalletCurrencyCard = ({ is_demo, currency, icon, icon_type, gradient_class }: TWalletCurrencyCard) => {
+ return (
+
+
+
+ );
+};
+
+export default WalletCurrencyCard;
diff --git a/packages/appstore/src/components/wallet-header/wallet-header-balance.tsx b/packages/appstore/src/components/wallet-header/wallet-header-balance.tsx
new file mode 100644
index 000000000000..d985a9b0cd06
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header-balance.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { Text, StatusBadge } from '@deriv/components';
+import { getStatusBadgeConfig } from '@deriv/account';
+import { useStore, observer } from '@deriv/stores';
+import { Localize } from '@deriv/translations';
+import { TWalletAccount } from 'Types';
+import { formatMoney } from '@deriv/shared';
+
+type TWalletHeaderBalance = Pick;
+
+const WalletHeaderBalance = observer(({ balance, currency }: TWalletHeaderBalance) => {
+ const {
+ traders_hub: { openFailedVerificationModal, multipliers_account_status, is_eu_user },
+ } = useStore();
+
+ const balance_amount = (
+
+
+
+ );
+
+ // TODO: just for test use empty object. When BE will be ready it will be fixed
+ const { text: badge_text, icon: badge_icon } = getStatusBadgeConfig(
+ multipliers_account_status,
+ openFailedVerificationModal,
+ {
+ platform: '',
+ category: '',
+ type: '',
+ jurisdiction: '',
+ }
+ );
+
+ return (
+
+ {multipliers_account_status && is_eu_user ? (
+
+ ) : (
+
+
+
+
+ {balance_amount}
+
+ )}
+
+ );
+});
+export default WalletHeaderBalance;
diff --git a/packages/appstore/src/components/wallet-header/wallet-header-buttons.tsx b/packages/appstore/src/components/wallet-header/wallet-header-buttons.tsx
new file mode 100644
index 000000000000..2a13cde524cd
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header-buttons.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import { observer, useStore } from '@deriv/stores';
+import { TWalletAccount, TWalletButton } from 'Types';
+import WalletButton from 'Components/wallet-button';
+
+type TWalletHeaderButtons = {
+ is_disabled: boolean;
+ is_open: boolean;
+ buttons: TWalletButton[];
+ wallet_account: TWalletAccount;
+};
+
+const WalletHeaderButtons = observer(({ is_disabled, is_open, buttons, wallet_account }: TWalletHeaderButtons) => {
+ const { ui, traders_hub } = useStore();
+ const { setIsWalletModalVisible } = ui;
+ const { setWalletModalActiveWalletID, setWalletModalActiveTab } = traders_hub;
+
+ return (
+
+ {buttons.map(button => {
+ button.action = () => {
+ setWalletModalActiveTab(button.name);
+ setIsWalletModalVisible(true);
+ setWalletModalActiveWalletID(wallet_account.loginid);
+ };
+
+ return (
+
+ );
+ })}
+
+ );
+});
+export default WalletHeaderButtons;
diff --git a/packages/appstore/src/components/wallet-header/wallet-header-title.tsx b/packages/appstore/src/components/wallet-header/wallet-header-title.tsx
new file mode 100644
index 000000000000..ad88f61d55f5
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header-title.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { Text, Badge } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import { TWalletAccount } from 'Types';
+
+type TWalletHeaderTitle = Pick;
+
+const WalletHeaderTitle = ({ is_demo, currency, landing_company_name }: TWalletHeaderTitle) => {
+ return (
+
+
+ {is_demo ? (
+
+ ) : (
+
+ )}
+
+ {!is_demo && (
+
+ )}
+
+ );
+};
+
+export default WalletHeaderTitle;
diff --git a/packages/appstore/src/components/wallet-header/wallet-header.scss b/packages/appstore/src/components/wallet-header/wallet-header.scss
new file mode 100644
index 000000000000..adbd6d34988c
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header.scss
@@ -0,0 +1,91 @@
+.wallet-header {
+ padding: 2.4rem;
+ background-color: var(--general-main-1);
+ border-radius: $BORDER_RADIUS * 4;
+ height: 12.8rem;
+
+ &__demo {
+ position: relative;
+ background-color: var(--wallet-demo-bg-color);
+ }
+ &__demo:before {
+ content: ' ';
+ display: block;
+ position: absolute;
+ inset: 0;
+ opacity: 0.1;
+ background-repeat: repeat;
+ background-position: 3% 30%;
+ border-radius: $BORDER_RADIUS * 4;
+ }
+
+ .theme--light &__demo:before {
+ background-image: url('./../../public/images/wallet-header-demo-bg.svg');
+ }
+
+ .theme--dark &__demo:before {
+ background-image: url('./../../public/images/wallet-header-demo-bg-dark.svg');
+ }
+
+ &__container {
+ position: relative;
+ display: flex;
+ height: 8rem;
+ }
+
+ &__currency {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 12.8rem;
+ margin-right: 2.4rem;
+ border-radius: $BORDER_RADIUS * 2;
+ }
+
+ &__description {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding-block: 0.5rem;
+
+ &-title {
+ display: flex;
+ align-items: center;
+ }
+
+ &-badge {
+ margin-left: 0.8rem;
+ }
+
+ &-buttons {
+ display: flex;
+ }
+ }
+
+ &__balance {
+ display: flex;
+ align-self: center;
+ padding-block: 1.3rem;
+ margin-left: auto;
+
+ &-title-amount {
+ display: flex;
+ flex-direction: column;
+ padding-inline: 2.4rem;
+
+ &-title {
+ align-self: flex-end;
+ }
+ }
+
+ &-arrow-icon {
+ cursor: pointer;
+ transition: transform 0.3s ease;
+ transform: rotate(0deg);
+ align-self: center;
+ }
+ &-arrow-icon-active {
+ transform: rotate(180deg);
+ }
+ }
+}
diff --git a/packages/appstore/src/components/wallet-header/wallet-header.tsx b/packages/appstore/src/components/wallet-header/wallet-header.tsx
new file mode 100644
index 000000000000..a8d7f9957247
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header.tsx
@@ -0,0 +1,81 @@
+import React from 'react';
+import { Icon } from '@deriv/components';
+import classNames from 'classnames';
+import WalletCurrencyCard from './wallet-currency-card';
+import WalletHeaderButtons from './wallet-header-buttons';
+import WalletHeaderTitle from './wallet-header-title';
+import WalletHeaderBalance from './wallet-header-balance';
+import { TWalletAccount } from 'Types';
+import { getWalletHeaderButtons } from 'Constants/utils';
+import { observer, useStore } from '@deriv/stores';
+import './wallet-header.scss';
+
+type TWalletHeader = {
+ wallet_account: TWalletAccount;
+};
+
+const WalletHeader = observer(({ wallet_account }: TWalletHeader) => {
+ const { client, traders_hub } = useStore();
+ const { switchAccount, loginid } = client;
+ const is_active = wallet_account.is_selected;
+ // const [is_loading, setIsLoading] = useState(false);
+ const { multipliers_account_status } = traders_hub;
+
+ const { is_demo, currency, gradient_card_class, currency_config, icon, balance, landing_company_name } =
+ wallet_account;
+
+ const wallet_buttons = getWalletHeaderButtons(wallet_account.is_demo);
+
+ const onArrowClickHandler = async () => {
+ // setIsLoading(true);
+ if (loginid !== wallet_account.loginid) await switchAccount(wallet_account.loginid);
+ // setIsLoading(false);
+ };
+
+ /** @todo: uncomment this when we have a skeleton loader for wallet header*/
+ // useEffect(() => {
+ // if (is_authorize) {
+ // setIsLoading(false);
+ // }
+ // }, [is_authorize]);}
+
+ return (
+
+ );
+});
+
+export default WalletHeader;
diff --git a/packages/appstore/src/components/wallet-jurisdiction-badge/__tests__/wallet-jurisdiction-badge.spec.tsx b/packages/appstore/src/components/wallet-jurisdiction-badge/__tests__/wallet-jurisdiction-badge.spec.tsx
new file mode 100644
index 000000000000..8c036a66244c
--- /dev/null
+++ b/packages/appstore/src/components/wallet-jurisdiction-badge/__tests__/wallet-jurisdiction-badge.spec.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import WalletJurisdictionBadge from '../wallet-jurisdiction-badge';
+import { render, screen } from '@testing-library/react';
+
+describe('WalletJurisdictionBadge', () => {
+ it('Should render demo badge', () => {
+ render();
+
+ expect(screen.getByText('Demo')).toBeInTheDocument();
+ });
+
+ it('Should render svg badge', () => {
+ render();
+
+ expect(screen.getByText('SVG')).toBeInTheDocument();
+ });
+
+ it('Should render malta badge', () => {
+ render();
+
+ expect(screen.getByText('MALTA')).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/wallet-jurisdiction-badge/index.ts b/packages/appstore/src/components/wallet-jurisdiction-badge/index.ts
new file mode 100644
index 000000000000..eb311bb13e05
--- /dev/null
+++ b/packages/appstore/src/components/wallet-jurisdiction-badge/index.ts
@@ -0,0 +1,3 @@
+import WalletJurisdictionBadge from './wallet-jurisdiction-badge';
+
+export { WalletJurisdictionBadge };
diff --git a/packages/appstore/src/components/wallet-jurisdiction-badge/wallet-jurisdiction-badge.tsx b/packages/appstore/src/components/wallet-jurisdiction-badge/wallet-jurisdiction-badge.tsx
new file mode 100644
index 000000000000..3447b05ea32d
--- /dev/null
+++ b/packages/appstore/src/components/wallet-jurisdiction-badge/wallet-jurisdiction-badge.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Badge } from '@deriv/components';
+import { localize } from '@deriv/translations';
+
+type TWalletJurisdictionBadge = {
+ is_demo: boolean;
+ shortcode?: string;
+};
+
+const WalletJurisdictionBadge = ({ is_demo, shortcode }: TWalletJurisdictionBadge) => {
+ return is_demo ? (
+
+ ) : (
+
+ );
+};
+
+export default WalletJurisdictionBadge;
diff --git a/packages/appstore/src/components/wallet-transfer/__tests__/wallet-transfer.spec.tsx b/packages/appstore/src/components/wallet-transfer/__tests__/wallet-transfer.spec.tsx
new file mode 100644
index 000000000000..888267235c12
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/__tests__/wallet-transfer.spec.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import WalletTransfer from '../wallet-transfer';
+import { APIProvider } from '@deriv/api';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import { render, screen } from '@testing-library/react';
+
+jest.mock('../transfer-account-selector', () => jest.fn(() => TransferAccountSelector
));
+
+jest.mock('@deriv/components', () => ({
+ ...jest.requireActual('@deriv/components'),
+ AmountInput: () => AmountInput
,
+}));
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useFetch: jest.fn(() => ({ data: undefined })),
+}));
+
+describe('WalletTransfer', () => {
+ const mock = mockStore({
+ client: {
+ loginid: 'CRW1030',
+ accounts: {
+ CRW1030: {
+ token: 'token',
+ },
+ },
+ },
+ });
+
+ it('Should render two amount inputs and two transfer account selectors', () => {
+ render(
+
+
+
+
+
+ );
+
+ expect(screen.getAllByText('AmountInput')).toHaveLength(2);
+ expect(screen.getAllByText('TransferAccountSelector')).toHaveLength(2);
+ });
+});
diff --git a/packages/appstore/src/components/wallet-transfer/index.ts b/packages/appstore/src/components/wallet-transfer/index.ts
new file mode 100644
index 000000000000..488f1606ef01
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/index.ts
@@ -0,0 +1,3 @@
+import WalletTransfer from './wallet-transfer';
+
+export default WalletTransfer;
diff --git a/packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-list.spec.tsx b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-list.spec.tsx
new file mode 100644
index 000000000000..93c945e7c9ee
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-list.spec.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import TransferAccountList from '../transfer-account-list';
+import { render, screen } from '@testing-library/react';
+
+jest.mock('../../wallet-transfer-tile/wallet-transfer-tile', () => jest.fn(() => WalletTransferTile
));
+
+describe('TransferAccountList', () => {
+ let mocked_props: React.ComponentProps;
+
+ beforeEach(() => {
+ mocked_props = {
+ is_mobile: false,
+ selected_account: {
+ account_type: 'wallet',
+ balance: 100,
+ currency: 'USD',
+ display_currency_code: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ icon: 'Icon',
+ is_demo: false,
+ loginid: 'CRW1000',
+ shortcode: 'svg',
+ type: 'fiat',
+ active_wallet_icon: 'Wallet Icon',
+ },
+ setIsListModalOpen: jest.fn(),
+ setSelectedAccount: jest.fn(),
+ transfer_accounts: {
+ trading_accounts: {
+ CR1000: {
+ account_type: 'trading',
+ balance: 10,
+ currency: 'USD',
+ display_currency_code: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ icon: 'Icon',
+ is_demo: false,
+ loginid: '1',
+ shortcode: 'svg',
+ type: 'fiat',
+ active_wallet_icon: 'IcCurrencyUsd',
+ },
+ MTR2000: {
+ account_type: 'mt5',
+ balance: 10,
+ currency: 'USD',
+ display_currency_code: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ icon: 'Icon',
+ is_demo: false,
+ loginid: '2',
+ shortcode: 'svg',
+ type: 'fiat',
+ active_wallet_icon: 'IcCurrencyUsd',
+ },
+ },
+
+ wallet_accounts: {
+ CRW1000: {
+ account_type: 'wallet',
+ balance: 10000,
+ currency: 'USD',
+ display_currency_code: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ icon: 'Icon',
+ is_demo: false,
+ loginid: '3',
+ shortcode: 'svg',
+ type: 'fiat',
+ active_wallet_icon: 'IcCurrencyUsd',
+ },
+ },
+ },
+ transfer_hint: 'Transfer hint',
+ wallet_name: 'USD Wallet',
+ };
+ });
+
+ it('Should render proper titles of transfer accounts', () => {
+ render();
+
+ expect(screen.getByText('Trading accounts linked with USD Wallet')).toBeInTheDocument();
+ expect(screen.getByText('Wallets')).toBeInTheDocument();
+ });
+
+ it('Should render proper amount of transfer accounts', () => {
+ render();
+
+ expect(screen.getAllByText('WalletTransferTile')).toHaveLength(3);
+ });
+
+ it('Should render transfer hint for Wallets account list', () => {
+ mocked_props.transfer_accounts = { ...mocked_props.transfer_accounts, trading_accounts: {} };
+ render();
+
+ expect(screen.getByText('Transfer hint')).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-selector.spec.tsx b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-selector.spec.tsx
new file mode 100644
index 000000000000..8784109c5814
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-selector.spec.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import TransferAccountSelector from '../transfer-account-selector';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from '@testing-library/react';
+
+jest.mock('../transfer-account-list', () => jest.fn(() => TransferAccountList
));
+jest.mock('../../wallet-transfer-tile', () => jest.fn(() => WalletTransferTile
));
+
+describe('TransferAccountSelector', () => {
+ let modal_root_el: HTMLDivElement, mocked_props: React.ComponentProps;
+
+ beforeAll(() => {
+ modal_root_el = document.createElement('div');
+ modal_root_el.setAttribute('id', 'modal_root');
+ document.body.appendChild(modal_root_el);
+ });
+
+ beforeEach(() => {
+ mocked_props = {
+ is_mobile: false,
+ is_wallet_name_visible: false,
+ label: 'Transfer from',
+ onSelectAccount: jest.fn(),
+ placeholder: 'Placeholder',
+ portal_id: 'modal_root',
+ setIsWalletNameVisible: jest.fn(),
+ transfer_accounts: {
+ trading_accounts: {},
+ wallet_accounts: {},
+ },
+ transfer_hint: 'Transfer hint',
+ value: undefined,
+ wallet_name: 'USD Wallet',
+ };
+ });
+
+ it('Should render placeholder, if there is no selected account', () => {
+ render();
+
+ expect(screen.getByText('Transfer from')).toBeInTheDocument();
+ expect(screen.getByText('Placeholder')).toBeInTheDocument();
+ expect(screen.getByTestId('dt_chevron_icon')).toBeInTheDocument();
+ });
+
+ it('Should render WalletTransferTile if the account was selected', () => {
+ mocked_props.value = {
+ active_wallet_icon: 'Icon',
+ display_currency_code: 'USD',
+ account_type: 'wallet',
+ balance: 100,
+ currency: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ is_demo: false,
+ loginid: '12345678',
+ shortcode: 'svg',
+ type: 'fiat',
+ icon: 'Wallet Icon',
+ };
+ render();
+
+ expect(screen.getByText('Transfer from')).toBeInTheDocument();
+ expect(screen.getByText('WalletTransferTile')).toBeInTheDocument();
+ expect(screen.getByTestId('dt_chevron_icon')).toBeInTheDocument();
+ });
+
+ it('Should render account selector transfer tile with default values default', () => {
+ render();
+
+ expect(screen.getByText('Transfer from')).toBeInTheDocument();
+ expect(screen.getByText('Placeholder')).toBeInTheDocument();
+ expect(screen.getByTestId('dt_chevron_icon')).toBeInTheDocument();
+ });
+
+ it('Should render TransferAccountList when the user is clicking on Transfer selector', () => {
+ render();
+
+ const el_transfer_tile = screen.getByTestId('dt_transfer_account_selector');
+ userEvent.click(el_transfer_tile);
+
+ expect(screen.getByText('TransferAccountList')).toBeInTheDocument();
+ });
+
+ it('Should render proper label', () => {
+ render();
+
+ expect(screen.getByText('Transfer from')).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/wallet-transfer/transfer-account-selector/index.ts b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/index.ts
new file mode 100644
index 000000000000..50a91c417029
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/index.ts
@@ -0,0 +1,3 @@
+import TransferAccountSelector from './transfer-account-selector';
+
+export default TransferAccountSelector;
diff --git a/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-list.tsx b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-list.tsx
new file mode 100644
index 000000000000..e7239d33d9db
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-list.tsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Text } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import WalletTransferTile from '../wallet-transfer-tile';
+import type { TTransferAccount } from 'Types';
+
+type TTransferAccountList = {
+ is_mobile?: boolean;
+ onSelectAccount?: (account: TTransferAccount) => void;
+ selected_account?: TTransferAccount;
+ setIsListModalOpen: (value: boolean) => void;
+ setSelectedAccount: React.Dispatch>;
+ transfer_accounts: Record<'trading_accounts' | 'wallet_accounts', Record>;
+ transfer_hint?: string | JSX.Element;
+ wallet_name?: string;
+};
+
+const TitleLine = () => ;
+
+const TransferAccountList = ({
+ is_mobile,
+ onSelectAccount,
+ selected_account,
+ setIsListModalOpen,
+ setSelectedAccount,
+ transfer_accounts,
+ transfer_hint,
+ wallet_name,
+}: TTransferAccountList) => {
+ const is_single_list = React.useMemo(
+ () =>
+ Object.keys(transfer_accounts).filter(
+ key => Object.keys(transfer_accounts[key as 'trading_accounts' | 'wallet_accounts']).length > 0
+ ).length === 1,
+ [transfer_accounts]
+ );
+
+ return (
+
+ {Object.keys(transfer_accounts).map((key, idx) => {
+ if (Object.values(transfer_accounts[key as 'trading_accounts' | 'wallet_accounts']).length === 0)
+ return null;
+
+ return (
+
+
+
+
+ {key === 'trading_accounts' ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {Object.values(transfer_accounts[key as 'trading_accounts' | 'wallet_accounts']).map(
+ account => (
+ {
+ setSelectedAccount(account);
+ if (account) onSelectAccount?.(account);
+ setIsListModalOpen(false);
+ }}
+ />
+ )
+ )}
+
+
+ {transfer_hint && (
+
+ {transfer_hint}
+
+ )}
+
+ );
+ })}
+
+ );
+};
+export default React.memo(TransferAccountList);
diff --git a/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.scss b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.scss
new file mode 100644
index 000000000000..acbaf273e617
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.scss
@@ -0,0 +1,173 @@
+.transfer-account-selector {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ background-color: var(--general-main-1);
+ padding: 0.8rem;
+ cursor: pointer;
+
+ &__value {
+ padding: 0;
+ }
+
+ &__chevron-icon {
+ display: flex;
+ margin-left: 1rem;
+
+ @include mobile {
+ margin-left: auto;
+ }
+ }
+
+ &__heading {
+ display: flex;
+ margin-bottom: 0.4rem;
+ }
+
+ &__heading-with-chevron {
+ display: flex;
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ margin-right: auto;
+ width: 100%;
+
+ @include mobile {
+ align-items: unset;
+ }
+ }
+
+ &__list {
+ border-bottom: 4px solid $color-grey-2;
+
+ &__container {
+ @include mobile {
+ padding: 0 1.6rem 1.6rem;
+ }
+
+ .transfer-hint {
+ margin: 1.6rem 0;
+ padding: 0 0.8rem;
+
+ @include mobile {
+ margin: 0.8rem 0;
+ padding: 0;
+ }
+ }
+ }
+
+ &--is-last,
+ &--is-single,
+ &--is-mobile {
+ border-bottom: none;
+ }
+
+ &--is-last:is(&--is-mobile) {
+ margin-bottom: 0.8rem;
+ }
+
+ &-items {
+ margin-bottom: 1.6rem;
+ padding: 0 0.8rem;
+
+ @include mobile {
+ margin-bottom: 0;
+ padding: 0;
+ }
+ }
+
+ &-header {
+ display: flex;
+ padding: 1.6rem 2.4rem 0.8rem;
+
+ @include mobile {
+ padding: 0.8rem 0.8rem 0.4rem;
+
+ &__title-line {
+ flex-grow: 1;
+ border-bottom: 1px solid $color-grey-2;
+ margin: 0 0 0.65rem 1rem;
+ }
+ }
+ }
+
+ &-tile {
+ padding: 1rem 2rem;
+ flex-direction: row;
+ align-items: center;
+ cursor: pointer;
+
+ @include mobile {
+ border-radius: unset;
+ padding: 0.8rem;
+ }
+
+ .wallet-transfer-tile__icon {
+ margin-right: 1.6rem;
+
+ @include mobile {
+ margin-right: 0.8rem;
+ }
+ }
+ }
+ }
+}
+
+// Overwrite modal style
+.dc-modal__container_transfer-account-selector__modal-header {
+ max-width: 40rem;
+ max-height: 52.8rem !important;
+
+ @include mobile {
+ max-width: unset;
+ max-height: unset !important;
+ }
+}
+
+.dc-modal-header--transfer-account-selector__modal-header {
+ border-bottom: 2px solid $color-grey-2;
+}
+
+#mobile_list_modal_root {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
+ display: none;
+ opacity: 0;
+
+ &:not(:empty) {
+ display: flex;
+ opacity: 1;
+ }
+
+ .dc-modal {
+ width: 100vw;
+
+ &__container {
+ max-width: unset !important;
+ border-radius: unset;
+ box-shadow: unset;
+ }
+
+ &-header {
+ border: none;
+ padding: 1.6rem 2.4rem 0rem;
+
+ &__title {
+ font-size: var(--text-size-xxs);
+ padding: 0;
+ margin-bottom: 0;
+ }
+
+ &__close {
+ padding: 0;
+ margin: 0;
+ height: auto;
+ width: auto;
+ }
+ }
+ }
+}
diff --git a/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.tsx b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.tsx
new file mode 100644
index 000000000000..b6bed30949af
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.tsx
@@ -0,0 +1,160 @@
+import React from 'react';
+import { Div100vhContainer, Icon, Modal, Text, ThemedScrollbars } from '@deriv/components';
+import TransferAccountList from './transfer-account-list';
+import WalletTransferTile from '../wallet-transfer-tile';
+import { WalletJurisdictionBadge } from 'Components/wallet-jurisdiction-badge';
+import type { TTransferAccount } from 'Types';
+import './transfer-account-selector.scss';
+
+type TTransferAccountSelectorProps = {
+ contentScrollHandler?: React.UIEventHandler;
+ is_mobile?: boolean;
+ is_wallet_name_visible?: boolean;
+ label?: string;
+ onSelectAccount?: (account: TTransferAccount) => void;
+ placeholder?: string;
+ portal_id?: string;
+ setIsWalletNameVisible?: (value: boolean) => void;
+ transfer_accounts: Record<'trading_accounts' | 'wallet_accounts', Record>;
+ transfer_hint?: string | JSX.Element;
+ value?: TTransferAccount;
+ wallet_name?: string;
+};
+
+type TAccountSelectorTransferTileProps = {
+ is_mobile?: boolean;
+ label?: string;
+ selected_account?: TTransferAccount;
+ placeholder?: string;
+};
+
+const ChevronIcon = () => {
+ return (
+
+
+
+ );
+};
+
+const AccountSelectorTransferTile = ({
+ is_mobile,
+ label,
+ placeholder,
+ selected_account,
+}: TAccountSelectorTransferTileProps) => {
+ return (
+
+
+
+
+ {label}
+
+
+ {is_mobile &&
}
+
+
+ {selected_account ? (
+
+ ) : (
+
+ {placeholder}
+
+ )}
+
+
+ {!is_mobile && (
+
+
+
+
+ )}
+
+ );
+};
+
+const TransferAccountSelector = ({
+ contentScrollHandler,
+ is_mobile,
+ is_wallet_name_visible,
+ label,
+ onSelectAccount,
+ placeholder,
+ portal_id,
+ setIsWalletNameVisible,
+ transfer_accounts = { trading_accounts: {}, wallet_accounts: {} },
+ transfer_hint,
+ value,
+ wallet_name,
+}: TTransferAccountSelectorProps) => {
+ const [is_list_modal_open, setIsListModalOpen] = React.useState(false);
+ const [selected_account, setSelectedAccount] = React.useState(value);
+
+ React.useEffect(() => {
+ setSelectedAccount(value);
+ }, [value]);
+
+ const openAccountsList = () => {
+ setIsListModalOpen(true);
+ };
+
+ const getHeightOffset = React.useCallback(() => {
+ const header_height = '16.2rem';
+ const collapsed_header_height = '12.2rem';
+ return is_wallet_name_visible ? header_height : collapsed_header_height;
+ }, [is_wallet_name_visible]);
+
+ return (
+
+
+
+
+
+
setIsWalletNameVisible?.(true)}
+ portalId={portal_id}
+ transition_timeout={is_mobile ? { enter: 250, exit: 0 } : 250}
+ title={label}
+ toggleModal={() => setIsListModalOpen(old => !old)}
+ >
+
+
+
+
+
+
+
+ );
+};
+
+export default TransferAccountSelector;
diff --git a/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/__tests__/wallet-transfer-tile.spec.tsx b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/__tests__/wallet-transfer-tile.spec.tsx
new file mode 100644
index 000000000000..c7719d5f6b7d
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/__tests__/wallet-transfer-tile.spec.tsx
@@ -0,0 +1,88 @@
+import React from 'react';
+import WalletTransferTile from '../wallet-transfer-tile';
+import userEvent from '@testing-library/user-event';
+import { render, screen } from '@testing-library/react';
+
+jest.mock('@deriv/components', () => ({
+ ...jest.requireActual('@deriv/components'),
+ AppLinkedWithWalletIcon: jest.fn(() => AppLinkedWithWalletIcon
),
+ WalletIcon: jest.fn(() => WalletIcon
),
+}));
+
+describe('WalletTransferTile', () => {
+ let mocked_props: Required>;
+
+ beforeEach(() => {
+ mocked_props = {
+ account: {
+ account_type: 'trading',
+ balance: 100,
+ currency: 'USD',
+ display_currency_code: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ icon: 'Icon',
+ is_demo: false,
+ shortcode: 'svg',
+ loginid: '12345678',
+ type: 'fiat',
+ active_wallet_icon: 'Wallet Icon',
+ },
+ className: 'classname',
+ has_hover: false,
+ icon_size: 'small',
+ is_active: false,
+ is_list_item: false,
+ is_mobile: false,
+ onClick: jest.fn(),
+ };
+ });
+
+ it('Should render merged icon (App with Wallet)', () => {
+ render();
+
+ expect(screen.getByText('AppLinkedWithWalletIcon')).toBeInTheDocument();
+ });
+
+ it('Should render single wallet icon, if there is wallet account type', () => {
+ mocked_props.account = { ...mocked_props.account, account_type: 'wallet' };
+ render();
+
+ expect(screen.getByText('WalletIcon')).toBeInTheDocument();
+ });
+
+ it('Should render jurisdiction in mobile view', () => {
+ mocked_props.is_list_item = false;
+ mocked_props.is_mobile = true;
+ render();
+
+ expect(screen.getByText('SVG')).toBeInTheDocument();
+ });
+
+ it('Should render jurisdiction in desktop view', () => {
+ mocked_props.is_list_item = true;
+ render();
+
+ expect(screen.getByText('SVG')).toBeInTheDocument();
+ });
+
+ it('Should render proper account label', () => {
+ render();
+
+ expect(screen.getByText('Deriv Apps')).toBeInTheDocument();
+ });
+
+ it('Should render proper account balance', () => {
+ render();
+
+ expect(screen.getByText('Balance: 100.00 USD')).toBeInTheDocument();
+ });
+
+ it('Should trigger onClick callback when the user is clicking on Wallet tile', () => {
+ render();
+
+ const el_wallet_tile = screen.getByTestId('dt_wallet_transfer_tile');
+ userEvent.click(el_wallet_tile);
+
+ expect(mocked_props.onClick).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/index.ts b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/index.ts
new file mode 100644
index 000000000000..e5851511bba4
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/index.ts
@@ -0,0 +1,3 @@
+import WalletTransferTile from './wallet-transfer-tile';
+
+export default WalletTransferTile;
diff --git a/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.scss b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.scss
new file mode 100644
index 000000000000..606c6b4f0890
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.scss
@@ -0,0 +1,48 @@
+.wallet-transfer-tile {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ background-color: var(--general-main-1);
+ border-radius: $BORDER_RADIUS;
+ flex-grow: 1;
+
+ @include mobile {
+ flex-direction: column;
+ align-items: flex-start;
+
+ &__icon-with-badge {
+ display: flex;
+ align-items: end;
+ margin-bottom: 0.4rem;
+ }
+ }
+
+ &--hover {
+ &:hover {
+ background-color: var(--general-hover);
+ }
+ }
+
+ &--active {
+ background-color: var(--state-active);
+ }
+
+ &--list-item-background {
+ background-color: var(--general-main-2);
+ }
+
+ &__icon {
+ margin-right: 0.8rem;
+ min-width: 4rem;
+
+ @include mobile {
+ margin-right: 0.4rem;
+ }
+ }
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ }
+}
diff --git a/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.tsx b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.tsx
new file mode 100644
index 000000000000..510eebfbc27c
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import classNames from 'classnames';
+import { Text, AppLinkedWithWalletIcon, WalletIcon } from '@deriv/components';
+import { formatMoney } from '@deriv/shared';
+import { Localize } from '@deriv/translations';
+import { getAccountName } from 'Constants/utils';
+import { WalletJurisdictionBadge } from 'Components/wallet-jurisdiction-badge';
+import type { TTransferAccount } from 'Types';
+import './wallet-transfer-tile.scss';
+
+type TIconSize =
+ | React.ComponentProps['size']
+ | React.ComponentProps['size'];
+
+type TWalletTileProps = {
+ account?: TTransferAccount;
+ className?: string;
+ has_hover?: boolean;
+ icon_size?: TIconSize;
+ is_active?: boolean;
+ is_list_item?: boolean;
+ is_mobile?: boolean;
+ onClick?: () => void;
+};
+
+const IconComponent = ({ account, icon_size }: TWalletTileProps) => {
+ if (account?.account_type === 'wallet') {
+ return account?.icon ? (
+ ['size']}
+ type={account?.type}
+ />
+ ) : null;
+ }
+
+ return account?.icon && account?.active_wallet_icon ? (
+ ['size']}
+ type={account?.type}
+ wallet_icon={account?.active_wallet_icon}
+ />
+ ) : null;
+};
+
+const Balance = ({ account, is_list_item, is_mobile }: TWalletTileProps) => {
+ if (account?.balance !== undefined) {
+ let size;
+ if (is_list_item) size = is_mobile ? 'xxxs' : 'xxs';
+ else size = is_mobile ? 'xxxxs' : 'xxxs';
+
+ return (
+
+
+
+ );
+ }
+
+ return null;
+};
+
+const Label = ({ account, is_list_item, is_mobile }: TWalletTileProps) => {
+ let size;
+ if (is_list_item) size = is_mobile ? 'xxs' : 'xs';
+ else size = is_mobile ? 'xxxxs' : 'xxxs';
+
+ return (
+
+ {getAccountName({ ...account })}
+
+ );
+};
+
+const WalletTransferTile = ({
+ account,
+ className,
+ has_hover,
+ icon_size = 'small',
+ is_active,
+ is_list_item,
+ is_mobile,
+ onClick,
+}: TWalletTileProps) => {
+ return (
+ onClick?.()}
+ >
+
+
+
+
+
+ {!is_list_item && is_mobile && (
+
+ )}
+
+
+
+
+
+
+
+ {is_list_item &&
}
+
+ );
+};
+
+export default React.memo(WalletTransferTile);
diff --git a/packages/appstore/src/components/wallet-transfer/wallet-transfer.scss b/packages/appstore/src/components/wallet-transfer/wallet-transfer.scss
new file mode 100644
index 000000000000..120976f2dc40
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/wallet-transfer.scss
@@ -0,0 +1,52 @@
+.wallet-transfer {
+ display: flex;
+ flex-direction: column;
+ max-width: 65rem;
+ width: 100%;
+ margin: 9.6rem auto 4.8rem;
+
+ @include mobile {
+ margin: 4.8rem auto;
+ }
+
+ &__tiles-container {
+ display: flex;
+ flex-direction: column;
+ }
+
+ &__divider {
+ border: 0.5px solid var(--border-normal);
+ margin: 0.8rem 0;
+ }
+
+ &__tile {
+ display: flex;
+ border: 1px solid var(--border-normal);
+ border-radius: $BORDER_RADIUS;
+
+ .amount-input-wrapper {
+ border-radius: $BORDER_RADIUS;
+ flex-basis: 50%;
+ justify-content: space-between;
+
+ @include mobile {
+ flex-basis: 60%;
+ }
+ }
+
+ .transfer-account-selector {
+ border-radius: $BORDER_RADIUS;
+ flex-basis: 50%;
+
+ @include mobile {
+ flex-basis: 40%;
+ }
+ }
+ }
+
+ &__transfer-button {
+ margin-top: 4.8rem;
+ display: flex;
+ justify-content: flex-end;
+ }
+}
diff --git a/packages/appstore/src/components/wallet-transfer/wallet-transfer.tsx b/packages/appstore/src/components/wallet-transfer/wallet-transfer.tsx
new file mode 100644
index 000000000000..f77d89aca091
--- /dev/null
+++ b/packages/appstore/src/components/wallet-transfer/wallet-transfer.tsx
@@ -0,0 +1,280 @@
+import React, { useEffect } from 'react';
+import classNames from 'classnames';
+import { Field, FieldProps, Formik, Form, FormikHelpers } from 'formik';
+import { AmountInput, Button, Loading, MessageList } from '@deriv/components';
+import { useWalletTransfer, useCurrencyConfig } from '@deriv/hooks';
+import { validNumber } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
+import { localize, Localize } from '@deriv/translations';
+import TransferAccountSelector from './transfer-account-selector';
+import { getAccountName } from 'Constants/utils';
+import type { TMessageItem } from 'Types';
+import './wallet-transfer.scss';
+
+type TWalletTransferProps = {
+ contentScrollHandler: React.UIEventHandler;
+ is_wallet_name_visible: boolean;
+ setIsWalletNameVisible: (value: boolean) => void;
+};
+
+const Divider = () => ;
+
+const initial_demo_balance = 10000.0;
+
+const ERROR_CODES = {
+ is_demo: {
+ between_min_max: 'BetweenMinMax',
+ insufficient_fund: 'InsufficientFund',
+ },
+};
+
+const WalletTransfer = observer(({ is_wallet_name_visible, setIsWalletNameVisible }: TWalletTransferProps) => {
+ const { client, ui, traders_hub } = useStore();
+ const { setWalletModalActiveTab } = traders_hub;
+ const { is_switching } = client;
+ const { is_mobile } = ui;
+
+ const { getConfig } = useCurrencyConfig();
+
+ const {
+ active_wallet,
+ is_accounts_loading,
+ from_account,
+ to_account,
+ to_account_list,
+ transfer_accounts,
+ setFromAccount,
+ setToAccount,
+ } = useWalletTransfer();
+
+ useEffect(() => {
+ if (!from_account?.loginid) {
+ setFromAccount(active_wallet);
+ }
+ }, [active_wallet, from_account, setFromAccount]);
+
+ const portal_id = is_mobile ? 'mobile_list_modal_root' : 'modal_root';
+
+ const is_amount_to_input_disabled = !to_account;
+
+ const active_wallet_name = getAccountName({ ...active_wallet });
+
+ const transfer_to_hint = React.useMemo(() => {
+ return to_account?.loginid === active_wallet?.loginid ? (
+
+ ) : (
+ ''
+ );
+ }, [active_wallet?.loginid, active_wallet_name, from_account, to_account?.loginid]);
+
+ const [message_list, setMessageList] = React.useState([]);
+
+ const clearErrorMessages = React.useCallback(
+ () => setMessageList(list => list.filter(el => el.type !== 'error')),
+ []
+ );
+
+ const appendMessage = (error_code: string, message: TMessageItem) => {
+ setMessageList(list => {
+ if (list.some(el => el.key === error_code)) return list;
+ return [...list, message];
+ });
+ };
+
+ const validateAmount = (amount: number) => {
+ clearErrorMessages();
+
+ if (!amount || is_amount_to_input_disabled || !active_wallet?.is_demo) return;
+
+ const { is_ok, message } = validNumber(amount.toString(), {
+ type: 'float',
+ decimals: getConfig(from_account?.currency ?? '')?.fractional_digits,
+ min: 1,
+ max: from_account?.balance,
+ });
+
+ const should_reset_balance =
+ active_wallet?.balance !== undefined &&
+ amount > active_wallet?.balance &&
+ active_wallet?.balance < initial_demo_balance;
+
+ if (from_account?.loginid === active_wallet.loginid && should_reset_balance) {
+ appendMessage(ERROR_CODES.is_demo.insufficient_fund, {
+ variant: 'with-action-button',
+ key: ERROR_CODES.is_demo.insufficient_fund,
+ button_label: localize('Reset balance'),
+ onClickHandler: () => setWalletModalActiveTab('Deposit'),
+ message: localize(
+ 'You have insufficient fund in the selected wallet, please reset your virtual balance'
+ ),
+ type: 'error',
+ });
+ } else if (!is_ok) {
+ //else if not wallet loginid and not is_ok message
+ appendMessage(ERROR_CODES.is_demo.between_min_max, {
+ variant: 'base',
+ key: ERROR_CODES.is_demo.between_min_max,
+ message: `${message} ${from_account?.display_currency_code}`,
+ type: 'error',
+ });
+ }
+ };
+
+ const onSelectFromAccount = React.useCallback(
+ (
+ account: typeof from_account,
+ resetForm: FormikHelpers<{
+ to_amount: number;
+ from_amount: number;
+ }>['resetForm']
+ ) => {
+ if (account?.loginid === from_account?.loginid) return;
+ setFromAccount(account);
+ if (account?.loginid === active_wallet?.loginid) {
+ setToAccount(undefined);
+ } else {
+ setToAccount(active_wallet);
+ }
+ clearErrorMessages();
+ resetForm();
+ },
+ [active_wallet, clearErrorMessages, from_account?.loginid, setFromAccount, setToAccount]
+ );
+
+ const onSelectToAccount = React.useCallback(
+ (
+ account: typeof to_account,
+ resetForm: FormikHelpers<{
+ to_amount: number;
+ from_amount: number;
+ }>['resetForm']
+ ) => {
+ if (account?.loginid === to_account?.loginid) return;
+ setToAccount(account);
+ clearErrorMessages();
+ resetForm();
+ },
+ [clearErrorMessages, setToAccount, to_account?.loginid]
+ );
+
+ if (is_accounts_loading || is_switching) {
+ return ;
+ }
+
+ return (
+
+
undefined}
+ validateOnBlur={false}
+ >
+ {({ setValues, values, resetForm }) => (
+
+ )}
+
+
+ );
+});
+
+export default WalletTransfer;
diff --git a/packages/appstore/src/components/wallet-withdrawal/__tests__/wallet-withdrawal.test.tsx b/packages/appstore/src/components/wallet-withdrawal/__tests__/wallet-withdrawal.test.tsx
new file mode 100644
index 000000000000..a6e9ec4f4b46
--- /dev/null
+++ b/packages/appstore/src/components/wallet-withdrawal/__tests__/wallet-withdrawal.test.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import WalletWithdrawal from '../wallet-withdrawal';
+import { mockStore } from '@deriv/stores';
+import CashierProviders from '@deriv/cashier/src/cashier-providers';
+import { useRequest } from '@deriv/api';
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useRequest: jest.fn(),
+}));
+
+// @ts-expect-error ignore this until find a way to make arguments as partial
+const mockUseRequest = useRequest as jest.MockedFunction>;
+
+const mock_store = mockStore({
+ client: {
+ email: 'john@company.com',
+ },
+ modules: { cashier: { transaction_history: { onMount: jest.fn() } } },
+});
+
+describe('WalletWithdrawal', () => {
+ test('should render the component', () => {
+ // @ts-expect-error ignore this until find a way to make arguments as partial
+ mockUseRequest.mockReturnValue({});
+
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(screen.queryByTestId('dt_empty_state_title')).toBeInTheDocument();
+ expect(screen.queryByTestId('dt_empty_state_description')).toBeInTheDocument();
+ expect(screen.queryByTestId('dt_empty_state_action')).toHaveTextContent('Send email');
+ });
+});
diff --git a/packages/appstore/src/components/wallet-withdrawal/index.ts b/packages/appstore/src/components/wallet-withdrawal/index.ts
new file mode 100644
index 000000000000..bab74c8c7b9b
--- /dev/null
+++ b/packages/appstore/src/components/wallet-withdrawal/index.ts
@@ -0,0 +1,3 @@
+import WalletWithdrawal from './wallet-withdrawal';
+
+export default WalletWithdrawal;
diff --git a/packages/appstore/src/components/wallet-withdrawal/wallet-withdrawal.tsx b/packages/appstore/src/components/wallet-withdrawal/wallet-withdrawal.tsx
new file mode 100644
index 000000000000..0344dfd24c92
--- /dev/null
+++ b/packages/appstore/src/components/wallet-withdrawal/wallet-withdrawal.tsx
@@ -0,0 +1,8 @@
+import React from 'react';
+import WithdrawalVerificationEmail from '@deriv/cashier/src/pages/withdrawal/withdrawal-verification-email';
+
+const WalletWithdrawal = () => {
+ return ;
+};
+
+export default WalletWithdrawal;
diff --git a/packages/appstore/src/components/wallets-banner/__tests__/wallets-banner.spec.tsx b/packages/appstore/src/components/wallets-banner/__tests__/wallets-banner.spec.tsx
new file mode 100644
index 000000000000..ccc8eb85e9ee
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/__tests__/wallets-banner.spec.tsx
@@ -0,0 +1,222 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import { TImageTestID } from 'Assets/svgs/wallets/image-types';
+import WalletsBannerUpgrade from '../wallets-banner-upgrade';
+import WalletsBannerUpgrading from '../wallets-banner-upgrading';
+import WalletsBannerReady from '../wallets-banner-ready';
+
+describe('', () => {
+ const mockRootStore = mockStore({
+ traders_hub: {
+ toggleWalletsUpgrade: true,
+ },
+ });
+
+ describe('Should render properly with right banner if status is eligible: ', () => {
+ const desktop: TImageTestID = 'dt_upgrade_desktop';
+ const mobile: TImageTestID = 'dt_upgrade_mobile';
+
+ it('Should render upgrade now button', async () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const btn = screen.getByRole('button', { name: /Upgrade now/i });
+ expect(btn).toBeInTheDocument();
+ });
+
+ it('Should render image properly for desktop', () => {
+ mockRootStore.ui.is_mobile = false;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop);
+ const mobile_image = screen.queryByTestId(mobile);
+
+ expect(desktop_image).toBeInTheDocument();
+ expect(mobile_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for mobile', () => {
+ mockRootStore.ui.is_mobile = true;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop);
+ const mobile_image = screen.queryByTestId(mobile);
+
+ expect(mobile_image).toBeInTheDocument();
+ expect(desktop_image).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Should render properly with right banner if status is in_progress: ', () => {
+ const desktop: TImageTestID = 'dt_upgrading_desktop';
+ const mobile: TImageTestID = 'dt_upgrading_mobile';
+ const desktop_eu: TImageTestID = 'dt_upgrading_desktop_eu';
+ const mobile_eu: TImageTestID = 'dt_upgrading_mobile_eu';
+
+ it('Should render right title', () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const title = screen.queryByText(/We're setting up your Wallets/i);
+
+ expect(title).toBeInTheDocument();
+ });
+
+ it('Should render loading dots', () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const loading_dots = screen.queryByTestId('dt_wallets_loading_dots');
+
+ expect(loading_dots).toBeInTheDocument();
+ });
+
+ it('Should render image properly for desktop for Non-EU', () => {
+ mockRootStore.ui.is_mobile = false;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop);
+ const mobile_image = screen.queryByTestId(mobile);
+
+ expect(desktop_image).toBeInTheDocument();
+ expect(mobile_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for mobile for Non-EU', () => {
+ mockRootStore.ui.is_mobile = true;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop);
+ const mobile_image = screen.queryByTestId(mobile);
+
+ expect(mobile_image).toBeInTheDocument();
+ expect(desktop_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for desktop for EU', () => {
+ mockRootStore.ui.is_mobile = false;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop_eu);
+ const mobile_image = screen.queryByTestId(mobile_eu);
+
+ expect(desktop_image).toBeInTheDocument();
+ expect(mobile_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for mobile for EU', () => {
+ mockRootStore.ui.is_mobile = true;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop_eu);
+ const mobile_image = screen.queryByTestId(mobile_eu);
+
+ expect(mobile_image).toBeInTheDocument();
+ expect(desktop_image).not.toBeInTheDocument();
+ });
+ });
+
+ describe('Should render properly with right banner if status is migrated: ', () => {
+ const desktop: TImageTestID = 'dt_ready_desktop';
+ const mobile: TImageTestID = 'dt_ready_mobile';
+ const desktop_eu: TImageTestID = 'dt_ready_desktop_eu';
+ const mobile_eu: TImageTestID = 'dt_ready_mobile_eu';
+
+ it('Should render right title', () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const title = screen.queryByText(/Your Wallets are ready/i);
+
+ expect(title).toBeInTheDocument();
+ });
+
+ it('Should render tick', () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const tick = screen.queryByTestId('dt_wallets_ready_tick');
+
+ expect(tick).toBeInTheDocument();
+ });
+
+ it('Should render right button', () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const btn = screen.queryByText('Log out');
+
+ expect(btn).toBeInTheDocument();
+ });
+
+ it('Should render image properly for desktop for Non-EU', () => {
+ mockRootStore.ui.is_mobile = false;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop);
+ const mobile_image = screen.queryByTestId(mobile);
+
+ expect(desktop_image).toBeInTheDocument();
+ expect(mobile_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for mobile for Non-EU', () => {
+ mockRootStore.ui.is_mobile = true;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop);
+ const mobile_image = screen.queryByTestId(mobile);
+
+ expect(mobile_image).toBeInTheDocument();
+ expect(desktop_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for desktop for EU', () => {
+ mockRootStore.ui.is_mobile = false;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop_eu);
+ const mobile_image = screen.queryByTestId(mobile_eu);
+
+ expect(desktop_image).toBeInTheDocument();
+ expect(mobile_image).not.toBeInTheDocument();
+ });
+
+ it('Should render image properly for mobile for EU', () => {
+ mockRootStore.ui.is_mobile = true;
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+ const desktop_image = screen.queryByTestId(desktop_eu);
+ const mobile_image = screen.queryByTestId(mobile_eu);
+
+ expect(mobile_image).toBeInTheDocument();
+ expect(desktop_image).not.toBeInTheDocument();
+ });
+
+ it('Should call logout function when click on button', async () => {
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+
+ const btn = screen.getByText('Log out');
+
+ await userEvent.click(btn);
+
+ expect(btn).toBeInTheDocument();
+ expect(mockRootStore.client.logout).toBeCalledTimes(1);
+ });
+ });
+});
diff --git a/packages/appstore/src/components/wallets-banner/index.ts b/packages/appstore/src/components/wallets-banner/index.ts
new file mode 100644
index 000000000000..5309a3973964
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/index.ts
@@ -0,0 +1,4 @@
+import WalletsBanner from './wallets-banner';
+import './wallets-banner.scss';
+
+export default WalletsBanner;
diff --git a/packages/appstore/src/components/wallets-banner/wallets-banner-ready.tsx b/packages/appstore/src/components/wallets-banner/wallets-banner-ready.tsx
new file mode 100644
index 000000000000..0501b929c73c
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/wallets-banner-ready.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import WalletsImage from 'Assets/svgs/wallets';
+import { Button, Icon, Text } from '@deriv/components';
+import { Localize, localize } from '@deriv/translations';
+import { TWalletsImagesListKey } from 'Assets/svgs/wallets/image-types';
+import { observer, useStore } from '@deriv/stores';
+
+// just for now for testing purpose, in the future 'is_eu' value will be taken from the store
+type TWalletsBannerReady = {
+ is_eu?: boolean;
+};
+
+const WalletsBannerReady = observer(({ is_eu }: TWalletsBannerReady) => {
+ const {
+ client: { logout },
+ ui: { is_mobile },
+ } = useStore();
+
+ const mobile_class = is_eu ? 'ready_mobile_eu' : 'ready_mobile';
+ const desktop_class = is_eu ? 'ready_desktop_eu' : 'ready_desktop';
+
+ const image: TWalletsImagesListKey = is_mobile ? mobile_class : desktop_class;
+
+ const title_size = is_mobile ? 'xs' : 'sm';
+ const description_size = is_mobile ? 'xxxs' : 'xs';
+ const tick_size = is_mobile ? 16 : 24;
+
+ const onButtonClickHandler = async () => {
+ await logout();
+ };
+
+ return (
+
+
+
+
+
+
]}
+ />
+
]}
+ />
+
+
+
+
+ );
+});
+
+export default WalletsBannerReady;
diff --git a/packages/appstore/src/components/wallets-banner/wallets-banner-upgrade.tsx b/packages/appstore/src/components/wallets-banner/wallets-banner-upgrade.tsx
new file mode 100644
index 000000000000..825891d1ce52
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/wallets-banner-upgrade.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import WalletsImage from 'Assets/svgs/wallets';
+import { Button, Text } from '@deriv/components';
+import { Localize, localize } from '@deriv/translations';
+import { TWalletsImagesListKey } from 'Assets/svgs/wallets/image-types';
+import { observer, useStore } from '@deriv/stores';
+
+const WalletsBannerUpgrade = observer(() => {
+ const { traders_hub, ui } = useStore();
+ const { toggleWalletsUpgrade } = traders_hub;
+ const { is_mobile } = ui;
+
+ const image: TWalletsImagesListKey = is_mobile ? 'upgrade_mobile' : 'upgrade_desktop';
+ const size: string = is_mobile ? 'xs' : 'm';
+
+ return (
+
+
+
+ , ]}
+ />
+
+
+
+
+ );
+});
+
+export default WalletsBannerUpgrade;
diff --git a/packages/appstore/src/components/wallets-banner/wallets-banner-upgrading.tsx b/packages/appstore/src/components/wallets-banner/wallets-banner-upgrading.tsx
new file mode 100644
index 000000000000..0158acc3f4fe
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/wallets-banner-upgrading.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import WalletsImage from 'Assets/svgs/wallets';
+import { Text } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import { TWalletsImagesListKey } from 'Assets/svgs/wallets/image-types';
+import { observer, useStore } from '@deriv/stores';
+
+// just for now for testing purpose, in the future 'is_eu' value will be taken from the store
+type TWalletsBannerUpgrading = {
+ is_eu?: boolean;
+};
+
+const WalletsBannerUpgrading = observer(({ is_eu }: TWalletsBannerUpgrading) => {
+ const { ui } = useStore();
+ const { is_mobile } = ui;
+
+ const mobile_class = is_eu ? 'upgrading_mobile_eu' : 'upgrading_mobile';
+ const desktop_class = is_eu ? 'upgrading_desktop_eu' : 'upgrading_desktop';
+ const image: TWalletsImagesListKey = is_mobile ? mobile_class : desktop_class;
+
+ const title_size = is_mobile ? 'xs' : 'sm';
+ const description_size = is_mobile ? 'xxxs' : 'xs';
+
+ return (
+
+
+
+
+
+
+
+
]}
+ />
+
]}
+ />
+
+
+
+ );
+});
+
+export default WalletsBannerUpgrading;
diff --git a/packages/appstore/src/components/wallets-banner/wallets-banner.scss b/packages/appstore/src/components/wallets-banner/wallets-banner.scss
new file mode 100644
index 000000000000..588446d13f80
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/wallets-banner.scss
@@ -0,0 +1,188 @@
+.wallets-banner {
+ &__container {
+ position: relative;
+ padding-inline: 4.8rem;
+ margin-bottom: 2.4rem;
+ overflow: hidden;
+
+ // TODO: remove this after the whole flow is finalized
+ background-color: var(--general-main-1);
+
+ border: 1px solid var(--wallets-banner-border-color);
+ box-shadow: var(--wallet-box-shadow);
+ border-radius: $BORDER_RADIUS * 4;
+
+ @include mobile {
+ padding: 1.4rem 1.6rem;
+ border-radius: $BORDER_RADIUS * 2;
+ }
+ }
+
+ &__image {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ border-top-right-radius: $BORDER_RADIUS * 4;
+ border-bottom-right-radius: $BORDER_RADIUS * 4;
+
+ @include mobile {
+ border-top-right-radius: $BORDER_RADIUS * 2;
+ border-bottom-right-radius: $BORDER_RADIUS * 2;
+ }
+ }
+
+ &__upgrade-banner {
+ height: 18rem;
+
+ @include mobile {
+ height: 10.8rem;
+ }
+
+ &-description {
+ width: 45%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: baseline;
+
+ @include mobile {
+ width: 70%;
+ }
+ }
+
+ &-button {
+ margin-top: 0.8rem;
+ padding-inline: 6.7rem;
+
+ @include mobile {
+ padding-inline: 1.6rem;
+ }
+ }
+ }
+
+ &__upgrading-banner {
+ display: flex;
+ align-items: center;
+ height: 18.2rem;
+
+ @include mobile {
+ height: 14.2rem;
+ }
+
+ &-description {
+ width: 45%;
+ height: 80%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ align-items: baseline;
+
+ @include mobile {
+ width: 70%;
+ }
+ }
+
+ &-loading {
+ span {
+ animation-name: loading;
+ animation-duration: 1.5s;
+ animation-iteration-count: infinite;
+ animation-timing-function: cubic-bezier(1, 0, 0, 1);
+ }
+
+ span:nth-child(2) {
+ animation-delay: 0.5s;
+ }
+
+ span:nth-child(3) {
+ animation-delay: 1s;
+ }
+ }
+
+ &-dot {
+ height: 0.8rem;
+ width: 0.8rem;
+ background-color: var(--wallets-banner-dot-color);
+ border-radius: 50%;
+ display: inline-block;
+ margin-right: 0.6rem;
+
+ @include mobile {
+ height: 0.5rem;
+ width: 0.5rem;
+ }
+ }
+
+ &-image {
+ opacity: 50%;
+ height: 100%;
+ }
+ }
+
+ &__ready-banner {
+ display: flex;
+ align-items: center;
+ height: 23rem;
+ background-color: var(--wallets-banner-ready-bg-color);
+
+ @include mobile {
+ height: 19rem;
+ }
+
+ &-tick {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background-color: var(--wallets-banner-ready-tick-bg-color);
+ border-radius: 50%;
+ width: 4.8rem;
+ height: 4.8rem;
+
+ @include mobile {
+ width: 3.2rem;
+ height: 3.2rem;
+ }
+ }
+
+ &-description {
+ width: 45%;
+ height: 80%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ align-items: baseline;
+
+ @include mobile {
+ width: 70%;
+ }
+ }
+
+ &-button {
+ margin-top: 0.8rem;
+ padding-inline: 8.6rem;
+
+ @include mobile {
+ padding-inline: 5rem;
+ }
+ }
+
+ &-image {
+ opacity: 90%;
+ height: 100%;
+
+ @include mobile {
+ opacity: 100%;
+ }
+ }
+ }
+}
+
+@keyframes loading {
+ 0% {
+ background-color: var(--wallets-banner-active-dot-color);
+ }
+ 100% {
+ opacity: var(--wallets-banner-dot-color);
+ }
+}
diff --git a/packages/appstore/src/components/wallets-banner/wallets-banner.tsx b/packages/appstore/src/components/wallets-banner/wallets-banner.tsx
new file mode 100644
index 000000000000..62561f8c0a84
--- /dev/null
+++ b/packages/appstore/src/components/wallets-banner/wallets-banner.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import WalletsBannerUpgrade from './wallets-banner-upgrade';
+import WalletsBannerUpgrading from './wallets-banner-upgrading';
+import WalletsBannerReady from './wallets-banner-ready';
+import { useWalletMigration } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+
+const WalletsBanner = observer(() => {
+ const { is_eligible, is_failed, is_in_progress, is_migrated } = useWalletMigration();
+ const { traders_hub } = useStore();
+ const { is_eu_user } = traders_hub;
+
+ if (is_migrated) return ;
+
+ if (is_eligible || is_failed) return ;
+
+ if (is_in_progress) return ;
+
+ return null;
+});
+
+export default WalletsBanner;
diff --git a/packages/appstore/src/constants/mock_wallet_migration_response.ts b/packages/appstore/src/constants/mock_wallet_migration_response.ts
new file mode 100644
index 000000000000..b557ff1be44b
--- /dev/null
+++ b/packages/appstore/src/constants/mock_wallet_migration_response.ts
@@ -0,0 +1,213 @@
+import { localize } from '@deriv/translations';
+import { getWalletCurrencyIcon } from './utils';
+
+const getMockWalletMigrationResponse = () => {
+ return [
+ {
+ title: localize('Non-EU USD accounts'),
+ wallets: [
+ {
+ wallet_details: {
+ balance: 0,
+ currency: 'USD',
+ icon: getWalletCurrencyIcon('USD', false),
+ icon_type: 'fiat',
+ jurisdiction_title: 'SVG',
+ name: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ },
+ account_list: [
+ {
+ balance: 1000,
+ currency: 'USD',
+ account_name: 'US Dollar',
+ icon: 'IcCurrencyUsd',
+ platform: 'deriv',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'MT5 Derived SVG',
+ icon: 'IcRebrandingMt5DerivedDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Derived',
+ landing_company_name: 'SVG',
+ },
+ {
+ balance: 123,
+ currency: 'USD',
+ account_name: 'MT5 Derived BVI',
+ icon: 'IcRebrandingMt5DerivedDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Derived',
+ landing_company_name: 'BVI',
+ },
+ {
+ balance: 20,
+ currency: 'USD',
+ account_name: 'MT5 Derived Vanuata',
+ icon: 'IcRebrandingMt5DerivedDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Derived',
+ landing_company_name: 'Vanuatu',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'MT5 Financial SVG',
+ icon: 'IcRebrandingMt5FinancialDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Financial',
+ landing_company_name: 'SVG',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'MT5 Financial BVI',
+ icon: 'IcRebrandingMt5FinancialDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Financial',
+ landing_company_name: 'SVG',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'MT5 Financial Vanuatu',
+ icon: 'IcRebrandingMt5FinancialDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Financial',
+ landing_company_name: 'SVG',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'MT5 Financial Labuan',
+ icon: 'IcRebrandingMt5FinancialDashboard',
+ platform: 'mt5',
+ sub_account_type: 'Financial',
+ landing_company_name: 'SVG',
+ },
+ {
+ balance: 150,
+ currency: 'USD',
+ account_name: 'MT5 Swap-free',
+ icon: 'IcRebrandingMt5SwapFree',
+ platform: 'mt5',
+ sub_account_type: 'Swap-Free',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'Deriv X',
+ icon: 'IcRebrandingDerivx',
+ platform: 'derivx',
+ },
+ {
+ balance: 100,
+ currency: 'USD',
+ account_name: 'Deriv EZ',
+ icon: 'IcDerivez',
+ platform: 'derivez',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: localize('EU-regulated USD accounts'),
+ wallets: [
+ {
+ wallet_details: {
+ balance: 0,
+ currency: 'USD',
+ icon: getWalletCurrencyIcon('USD', false),
+ icon_type: 'fiat',
+ jurisdiction_title: 'MALTA',
+ name: 'USD',
+ gradient_class: 'wallet-card__usd-bg',
+ },
+ account_list: [
+ {
+ balance: 1000,
+ currency: 'USD',
+ account_name: 'US Dollar',
+ icon: 'IcCurrencyUsd',
+ platform: 'deriv',
+ },
+ {
+ balance: 234,
+ currency: 'USD',
+ account_name: 'MT5 CFDs',
+ icon: 'IcRebrandingMt5Cfds',
+ platform: 'mt5',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ title: localize('Cryptocurrency accounts'),
+ wallets: [
+ {
+ wallet_details: {
+ balance: 0,
+ currency: 'BTC',
+ icon: getWalletCurrencyIcon('BTC', false),
+ icon_type: 'crypto',
+ jurisdiction_title: 'SVG',
+ name: 'Bitcoin',
+ gradient_class: 'wallet-card__btc-bg',
+ },
+ account_list: [
+ {
+ balance: 0.00212012,
+ currency: 'BTC',
+ account_name: 'Bitcoin',
+ icon: 'IcCurrencyBtc',
+ },
+ ],
+ },
+ {
+ wallet_details: {
+ balance: 0,
+ currency: 'ETH',
+ icon: getWalletCurrencyIcon('ETH', false),
+ icon_type: 'crypto',
+ jurisdiction_title: 'SVG',
+ name: 'Ethereum',
+ gradient_class: 'wallet-card__eth-bg',
+ },
+ account_list: [
+ {
+ balance: 0.00212012,
+ currency: 'ETH',
+ account_name: 'Ethereum',
+ icon: 'IcCurrencyEth',
+ },
+ ],
+ },
+ {
+ wallet_details: {
+ balance: 0,
+ currency: 'USDC',
+ icon: getWalletCurrencyIcon('USDC', false),
+ icon_type: 'crypto',
+ jurisdiction_title: 'SVG',
+ name: 'USD Coin',
+ gradient_class: 'wallet-card__usdc-bg',
+ },
+ account_list: [
+ {
+ balance: 0.00212012,
+ currency: 'USDC',
+ account_name: 'USD Coin',
+ icon: 'IcCurrencyUsdc',
+ },
+ ],
+ },
+ ],
+ },
+ ];
+};
+
+export default getMockWalletMigrationResponse;
diff --git a/packages/appstore/src/constants/routes-config.ts b/packages/appstore/src/constants/routes-config.ts
deleted file mode 100644
index 42109b352a64..000000000000
--- a/packages/appstore/src/constants/routes-config.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { localize } from '@deriv/translations';
-import TradersHub from 'Modules/traders-hub';
-import ConfigStore from 'Stores/config-store';
-import { TRoute } from 'Types';
-import Onboarding from 'Modules/onboarding';
-import CFDCompareAccounts from '@deriv/cfd/src/Containers/cfd-compare-accounts';
-
-type TRoutesConfig = {
- consumer_routes: ConfigStore['routes'];
-};
-
-// 1. Order matters! Put more specific consumer_routes at the top.
-// 2. Don't use `Localize` component since native html tag like `option` cannot render them
-const initRoutesConfig = ({ consumer_routes }: TRoutesConfig): TRoute[] => [
- {
- path: consumer_routes.traders_hub,
- component: TradersHub,
- getTitle: () => localize("Trader's Hub"),
- },
- {
- path: consumer_routes.onboarding,
- component: Onboarding,
- getTitle: () => localize('Onboarding'),
- },
- {
- path: consumer_routes.compare_cfds,
- component: CFDCompareAccounts,
- getTitle: () => localize('CFDCompareAccounts'),
- },
-];
-
-let routes_config: Array;
-
-const getRoutesConfig = ({ consumer_routes }: TRoutesConfig): TRoute[] => {
- // For default page route if page/path is not found, must be kept at the end of routes_config array.
- if (!routes_config) {
- const route_default = { getTitle: () => localize('Error 404') };
-
- routes_config = initRoutesConfig({ consumer_routes });
- routes_config.push(route_default);
- }
-
- return routes_config;
-};
-export default getRoutesConfig;
diff --git a/packages/appstore/src/constants/upgrade-info-lists-config.tsx b/packages/appstore/src/constants/upgrade-info-lists-config.tsx
new file mode 100644
index 000000000000..468529bdd6d5
--- /dev/null
+++ b/packages/appstore/src/constants/upgrade-info-lists-config.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import { Text } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+
+type TUpgradeInformationList = {
+ is_eu: boolean;
+ text_info_size: string;
+ form_line_height: string;
+};
+
+const getUpgradeInformationList = ({ is_eu, text_info_size, form_line_height }: TUpgradeInformationList) => [
+ {
+ name: 'upgrade_info',
+ visibility: true,
+ content: (
+
+ ),
+ },
+ {
+ name: 'open_positions',
+ visibility: true,
+ content: ,
+ },
+ {
+ name: 'deriv_p2p',
+ visibility: !is_eu,
+ content: (
+ }
+ />
+ ),
+ },
+ {
+ name: 'payment_agents',
+ visibility: !is_eu,
+ content: (
+ }
+ />
+ ),
+ },
+];
+
+export default getUpgradeInformationList;
diff --git a/packages/appstore/src/constants/utils.ts b/packages/appstore/src/constants/utils.ts
index db86a3d13da9..d089409d2c29 100644
--- a/packages/appstore/src/constants/utils.ts
+++ b/packages/appstore/src/constants/utils.ts
@@ -1,4 +1,6 @@
import { isMobile } from '@deriv/shared';
+import { localize } from '@deriv/translations';
+import { TTransferAccount, TWalletButton } from 'Types';
/**
* This function checks whether the current item should have a border at the bottom 'aka "divider" '.
@@ -20,3 +22,125 @@ export const getHasDivider = (current_item_index: number, list_size: number, ava
(list_size % available_grid_columns === 0 ? available_grid_columns : list_size % available_grid_columns)
);
};
+
+// TODO: Moved to shared package! Delete it later, right now it uses for cashier wallet modals
+// TODO: Refactor using data transformation layer pattern when we will have API for wallets (e.g. wallet.icon)
+export const getWalletCurrencyIcon = (currency: string, is_dark_mode_on: boolean, is_modal = false) => {
+ switch (currency) {
+ case 'demo':
+ if (is_modal) return 'IcWalletDerivDemoLight';
+ return is_dark_mode_on ? 'IcWalletDerivDemoDark' : 'IcWalletDerivDemoLight';
+ case 'USD':
+ return 'IcWalletCurrencyUsd';
+ case 'EUR':
+ return 'IcWalletCurrencyEur';
+ case 'AUD':
+ return 'IcWalletCurrencyAud';
+ case 'GBP':
+ return 'IcWalletCurrencyGbp';
+ case 'BTC':
+ return is_dark_mode_on ? 'IcWalletBitcoinDark' : 'IcWalletBitcoinLight';
+ case 'ETH':
+ return is_dark_mode_on ? 'IcWalletEtheriumDark' : 'IcWalletEtheriumLight';
+ case 'USDT':
+ case 'eUSDT':
+ case 'tUSDT':
+ case 'UST':
+ if (is_modal) {
+ return is_dark_mode_on ? 'IcWalletModalTetherDark' : 'IcWalletModalTetherLight';
+ }
+ return is_dark_mode_on ? 'IcWalletTetherDark' : 'IcWalletTetherLight';
+ case 'LTC':
+ return is_dark_mode_on ? 'IcWalletLiteCoinDark' : 'IcWalletLiteCoinLight';
+ case 'USDC':
+ return is_dark_mode_on ? 'IcWalletUsdCoinDark' : 'IcWalletUsdCoinLight';
+ default:
+ return 'Unknown';
+ }
+};
+
+export const getWalletHeaderButtons = (is_demo: boolean, handleAction?: () => void): TWalletButton[] => {
+ return is_demo
+ ? [
+ {
+ name: 'Transfer',
+ text: localize('Transfer'),
+ icon: 'IcAccountTransfer',
+ action: () => handleAction?.(),
+ },
+ {
+ name: 'Transactions',
+ text: localize('Transactions'),
+ icon: 'IcStatement',
+ action: () => handleAction?.(),
+ },
+ {
+ name: 'Deposit',
+ text: localize('Reset balance'),
+ icon: 'IcCashierAdd',
+ action: () => handleAction?.(),
+ },
+ ]
+ : [
+ {
+ name: 'Deposit',
+ text: localize('Deposit'),
+ icon: 'IcCashierAdd',
+ action: () => handleAction?.(),
+ },
+ {
+ name: 'Withdraw',
+ text: localize('Withdraw'),
+ icon: 'IcCashierMinus',
+ action: () => handleAction?.(),
+ },
+ {
+ name: 'Transfer',
+ text: localize('Transfer'),
+ icon: 'IcAccountTransfer',
+ action: () => handleAction?.(),
+ },
+ {
+ name: 'Transactions',
+ text: localize('Transactions'),
+ icon: 'IcStatement',
+ action: () => handleAction?.(),
+ },
+ ];
+};
+
+export const getAccountName = ({
+ account_type,
+ mt5_market_type,
+ display_currency_code,
+}: Partial>): string => {
+ switch (account_type) {
+ case 'trading':
+ return localize('Deriv Apps');
+ case 'mt5': {
+ switch (mt5_market_type) {
+ case 'financial':
+ return localize('MT5 Financial');
+ case 'synthetic':
+ return localize('MT5 Derived');
+ case 'all':
+ return localize('MT5 Swap-free');
+ default:
+ return '';
+ }
+ }
+ case 'derivez':
+ return localize('Deriv EZ');
+ case 'dxtrade':
+ return localize('Deriv X');
+ // @ts-expect-error Need to update @deriv/api-types to fix the TS error
+ case 'ctrader':
+ return localize('Deriv cTrader');
+ case 'wallet':
+ return localize('{{display_currency_code}} Wallet', {
+ display_currency_code,
+ });
+ default:
+ return '';
+ }
+};
diff --git a/packages/appstore/src/constants/wallet-mocked-response.ts b/packages/appstore/src/constants/wallet-mocked-response.ts
new file mode 100644
index 000000000000..ba8ab7d627d7
--- /dev/null
+++ b/packages/appstore/src/constants/wallet-mocked-response.ts
@@ -0,0 +1,43 @@
+// TODO: Remove this file once we have the real API response
+const wallets = [
+ {
+ name: 'USD Wallet',
+ currency: 'usd',
+ icon: 'IcCurrencyUsd',
+ balance: 100000,
+ icon_type: 'fiat',
+ state: 'default',
+ jurisdiction_title: 'svg',
+ },
+ {
+ name: 'USD Wallet',
+ currency: 'usd',
+ icon: 'IcCurrencyUsd',
+ balance: 100000,
+ icon_type: 'fiat',
+ state: 'default',
+ jurisdiction_title: 'svg',
+ },
+ {
+ name: 'MT5 Derived Demo',
+ currency: 'usd',
+ icon: 'IcRebrandingMt5Logo',
+ wallet_icon: 'IcWalletDerivDemoLight',
+ balance: 879,
+ icon_type: 'app',
+ app: 'mt5',
+ state: 'default',
+ is_demo: true,
+ },
+ {
+ name: 'Bitcoin Wallet',
+ currency: 'btc',
+ icon: 'IcCashierBitcoinLight',
+ balance: 0.003546,
+ icon_type: 'crypto',
+ state: 'default',
+ jurisdiction_title: 'svg',
+ },
+];
+
+export default wallets;
diff --git a/packages/appstore/src/constants/wallet_description_mapper.ts b/packages/appstore/src/constants/wallet_description_mapper.ts
new file mode 100644
index 000000000000..914c2d1a63af
--- /dev/null
+++ b/packages/appstore/src/constants/wallet_description_mapper.ts
@@ -0,0 +1,27 @@
+import { localize } from '@deriv/translations';
+
+type TWalletDescriptionMapper = {
+ [key: string]: string;
+};
+
+const wallet_description_mapper: TWalletDescriptionMapper = {
+ AUD: localize('Deposit and withdraw Australian dollars using credit or debit cards, e-wallets, or bank wires.'),
+ EUR: localize(
+ 'Deposit and withdraw euros into your accounts regulated by MFSA using credit or debit cards and e-wallets.'
+ ),
+ USD: localize('Deposit and withdraw US dollars using credit or debit cards, e-wallets, or bank wires.'),
+ BTC: localize(
+ "Deposit and withdraw Bitcoin, the world's most popular cryptocurrency, hosted on the Bitcoin blockchain."
+ ),
+ ETH: localize('Deposit and withdraw Ether, the fastest growing cryptocurrency, hosted on the Ethereum blockchain.'),
+ LTC: localize(
+ 'Deposit and withdraw Litecoin, the cryptocurrency with low transaction fees, hosted on the Litecoin blockchain.'
+ ),
+ USDC: localize('Deposit and withdraw USD Coin, hosted on the Ethereum blockchain.'),
+ eUSDT: localize('Deposit and withdraw Tether ERC20, a version of Tether hosted on the Ethereum blockchain.'),
+ tUSDT: localize('Deposit and withdraw Tether TRC20, a version of Tether hosted on the TRON blockchain.'),
+ UST: localize('Deposit and withdraw Tether Omni, hosted on the Bitcoin blockchain.'),
+ PaymentAgent: localize('Deposit and withdraw funds via authorised, independent payment agents.'),
+};
+
+export default wallet_description_mapper;
diff --git a/packages/appstore/src/constants/wallets-intro-content-config.tsx b/packages/appstore/src/constants/wallets-intro-content-config.tsx
new file mode 100644
index 000000000000..9cc822735c3c
--- /dev/null
+++ b/packages/appstore/src/constants/wallets-intro-content-config.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import WalletsImage from 'Assets/svgs/wallets';
+import { localize, Localize } from '@deriv/translations';
+
+const getWalletsIntroContent = (is_eu: boolean) => [
+ {
+ image: ,
+ title: localize('Introducing Wallets'),
+ description: is_eu ? localize('A new way to manage your funds') : localize('A better way to manage your funds'),
+ bullets: [
+ is_eu ? localize('One Wallet for all your transactions') : localize('One Wallet, one currency'),
+ is_eu
+ ? localize('Keep track of your trading funds in one place')
+ : localize('A Wallet for each currency to focus your funds'),
+ !is_eu && (
+
+ ),
+ ],
+ },
+ {
+ image: ,
+ title: localize('How it works'),
+ description: is_eu ? localize('Simply add your funds and trade') : localize('Get a Wallet, add funds, trade'),
+ bullets: [
+ !is_eu && localize('Get a Wallet for the currency you want'),
+ localize('Add funds to your Wallet via your favourite payment method'),
+ localize('Move funds to your trading account to start trading'),
+ ],
+ },
+ {
+ image: ,
+ title: localize('What happens to my trading accounts'),
+ description: localize("We'll link them"),
+ bullets: [
+ is_eu
+ ? localize("We'll connect your existing USD trading account(s) to your new USD Wallet ")
+ : localize("We'll connect your existing trading accounts of the same currency to your new Wallet"),
+ !is_eu && localize('For example, all your USD trading account(s) will be linked to your USD Wallet'),
+ ],
+ },
+];
+
+export default getWalletsIntroContent;
diff --git a/packages/appstore/src/modules/traders-hub/__tests__/index.spec.tsx b/packages/appstore/src/modules/traders-hub/__tests__/index.spec.tsx
index fd2f3eddabd5..bbe3c8f1b4ad 100644
--- a/packages/appstore/src/modules/traders-hub/__tests__/index.spec.tsx
+++ b/packages/appstore/src/modules/traders-hub/__tests__/index.spec.tsx
@@ -2,17 +2,22 @@ import React from 'react';
import { ContentFlag } from '@deriv/shared';
import { StoreProvider, mockStore } from '@deriv/stores';
import { render, screen } from '@testing-library/react';
+import { APIProvider } from '@deriv/api';
import TradersHub from '..';
jest.mock('Components/modals/modal-manager', () => jest.fn(() => 'mockedModalManager'));
+jest.mock('Components/main-title-bar', () => jest.fn(() => 'mockedMainTitleBar'));
jest.mock('Components/cfds-listing', () => jest.fn(() => 'mockedCFDsListing'));
jest.mock('Components/options-multipliers-listing', () => jest.fn(() => 'mocked'));
+jest.mock('../../tour-guide/tour-guide', () => jest.fn(() => 'mocked'));
describe('TradersHub', () => {
const render_container = (mock_store_override = {}) => {
const mock_store = mockStore(mock_store_override);
const wrapper = ({ children }: { children: JSX.Element }) => (
- {children}
+
+ {children}
+
);
return render(, {
diff --git a/packages/appstore/src/modules/traders-hub/index.tsx b/packages/appstore/src/modules/traders-hub/index.tsx
index ac99a42f880d..c18f45c1ac9a 100644
--- a/packages/appstore/src/modules/traders-hub/index.tsx
+++ b/packages/appstore/src/modules/traders-hub/index.tsx
@@ -12,7 +12,7 @@ import classNames from 'classnames';
import TourGuide from '../tour-guide/tour-guide';
import './traders-hub.scss';
-const TradersHub = () => {
+const TradersHub = observer(() => {
const { traders_hub, client, ui } = useStore();
const { notification_messages_ui: Notifications, is_mobile } = ui;
const { is_landing_company_loaded, is_logged_in, is_switching, is_logging_in, is_account_setting_loaded } = client;
@@ -123,6 +123,6 @@ const TradersHub = () => {
)}
>
);
-};
+});
-export default observer(TradersHub);
+export default TradersHub;
diff --git a/packages/appstore/src/modules/wallets/desktop-wallets-list.tsx b/packages/appstore/src/modules/wallets/desktop-wallets-list.tsx
new file mode 100644
index 000000000000..7889ef84ddff
--- /dev/null
+++ b/packages/appstore/src/modules/wallets/desktop-wallets-list.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { useWalletsList } from '@deriv/hooks';
+import { observer } from '@deriv/stores';
+import Wallet from 'Components/containers/wallet';
+
+const DesktopWalletsList = observer(() => {
+ const { data } = useWalletsList();
+
+ return (
+
+ {data?.map(wallet => (
+
+ ))}
+
+ );
+});
+
+export default DesktopWalletsList;
diff --git a/packages/appstore/src/modules/wallets/index.ts b/packages/appstore/src/modules/wallets/index.ts
new file mode 100644
index 000000000000..b8cb10bdc8c3
--- /dev/null
+++ b/packages/appstore/src/modules/wallets/index.ts
@@ -0,0 +1 @@
+export { default as WalletsModule } from './wallets';
diff --git a/packages/appstore/src/modules/wallets/mobile-wallets-carousel.tsx b/packages/appstore/src/modules/wallets/mobile-wallets-carousel.tsx
new file mode 100644
index 000000000000..df31999b94e8
--- /dev/null
+++ b/packages/appstore/src/modules/wallets/mobile-wallets-carousel.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { ButtonToggle } from '@deriv/components';
+import { useActiveWallet, useContentFlag } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import ButtonToggleLoader from 'Components/pre-loader/button-toggle-loader';
+import WalletCardsCarousel from 'Components/wallet-cards-carousel';
+import WalletCFDsListing from 'Components/wallet-content/wallet-cfds-listing';
+import WalletOptionsAndMultipliersListing from 'Components/wallet-content/wallet-option-multipliers-listing';
+import classNames from 'classnames';
+
+const MobileWalletsCarousel = observer(() => {
+ const { client, traders_hub } = useStore();
+ const { is_landing_company_loaded } = client;
+ const { selected_platform_type, setTogglePlatformType, is_eu_user } = traders_hub;
+ const { is_eu_demo, is_eu_real } = useContentFlag();
+ const eu_title = is_eu_demo || is_eu_real || is_eu_user;
+ const active_wallet = useActiveWallet();
+
+ const platform_toggle_options = [
+ { text: 'CFDs', value: 'cfd' },
+ { text: eu_title ? 'Multipliers' : 'Options & Multipliers', value: 'options' },
+ ];
+
+ const platformTypeChange = (event: { target: { value: string; name: string } }) => {
+ setTogglePlatformType(event.target.value);
+ };
+
+ return (
+ <>
+
+
+ {is_landing_company_loaded ? (
+
+ ) : (
+
+ )}
+ {selected_platform_type === 'cfd' && }
+ {selected_platform_type === 'options' && }
+
+ >
+ );
+});
+
+export default MobileWalletsCarousel;
diff --git a/packages/appstore/src/modules/wallets/wallets.scss b/packages/appstore/src/modules/wallets/wallets.scss
new file mode 100644
index 000000000000..a152a1057e9c
--- /dev/null
+++ b/packages/appstore/src/modules/wallets/wallets.scss
@@ -0,0 +1,25 @@
+.wallets-module {
+ display: flex;
+ min-height: calc(100vh - 84px);
+ background-color: var(--general-section-1);
+
+ @include mobile {
+ min-height: 100vh;
+ }
+
+ &__content {
+ width: 100%;
+ max-width: 131.2rem;
+ margin: auto;
+ display: flex;
+ padding: 4rem;
+ flex-direction: column;
+ align-items: center;
+ gap: 2.4rem;
+ align-self: stretch;
+
+ @include mobile {
+ padding: 2.4rem 0;
+ }
+ }
+}
diff --git a/packages/appstore/src/modules/wallets/wallets.tsx b/packages/appstore/src/modules/wallets/wallets.tsx
new file mode 100644
index 000000000000..7704a25adaa6
--- /dev/null
+++ b/packages/appstore/src/modules/wallets/wallets.tsx
@@ -0,0 +1,37 @@
+import React, { useEffect } from 'react';
+import { ThemedScrollbars, Loading } from '@deriv/components';
+import { useActiveWallet, useWalletsList } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import AddMoreWallets from 'Components/add-more-wallets';
+import ModalManager from 'Components/modals/modal-manager';
+import DesktopWalletsList from './desktop-wallets-list';
+import MobileWalletsCarousel from './mobile-wallets-carousel';
+import './wallets.scss';
+
+const Wallets = observer(() => {
+ const { client, ui } = useStore();
+ const { switchAccount, is_authorize } = client;
+ const { is_mobile } = ui;
+ const { data } = useWalletsList();
+ const active_wallet = useActiveWallet();
+
+ useEffect(() => {
+ if (!active_wallet && data && data?.length) {
+ switchAccount(data[0].loginid);
+ }
+ }, [active_wallet, data, switchAccount]);
+
+ if (!is_authorize) return ;
+
+ return (
+
+
+
+
+ );
+});
+
+export default Wallets;
diff --git a/packages/appstore/src/public/images/wallet-header-demo-bg-dark.svg b/packages/appstore/src/public/images/wallet-header-demo-bg-dark.svg
new file mode 100644
index 000000000000..0689dc76a1c5
--- /dev/null
+++ b/packages/appstore/src/public/images/wallet-header-demo-bg-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/appstore/src/public/images/wallet-header-demo-bg.svg b/packages/appstore/src/public/images/wallet-header-demo-bg.svg
new file mode 100644
index 000000000000..1625156e5725
--- /dev/null
+++ b/packages/appstore/src/public/images/wallet-header-demo-bg.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/appstore/src/services/websocket.ts b/packages/appstore/src/services/websocket.ts
deleted file mode 100644
index b2ac6e22830e..000000000000
--- a/packages/appstore/src/services/websocket.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { DerivWS } from 'Types';
-
-let ws: DerivWS = null;
-
-export const useWs = (): DerivWS => ws;
-
-export const initWs = (ws_instance: DerivWS): void => (ws = ws_instance);
diff --git a/packages/appstore/src/stores/base-store.ts b/packages/appstore/src/stores/base-store.ts
deleted file mode 100644
index b24a80ad456d..000000000000
--- a/packages/appstore/src/stores/base-store.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import RootStore from './root-store';
-
-export default class BaseStore {
- public root_store: RootStore;
-
- public constructor(root_store: RootStore) {
- this.root_store = root_store;
- }
-}
diff --git a/packages/appstore/src/stores/config-store.ts b/packages/appstore/src/stores/config-store.ts
deleted file mode 100644
index 7b92e450171a..000000000000
--- a/packages/appstore/src/stores/config-store.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { TConfigProps } from 'Types';
-import BaseStore from './base-store';
-
-export default class ConfigStore extends BaseStore {
- public has_router = true;
- public routes = {
- traders_hub: '/appstore/traders-hub',
- onboarding: '/appstore/onboarding',
- compare_cfds: '/appstore/compare-accounts',
-
- my_apps: '/my-apps',
- explore: '/explore',
- about_us: '/about-us',
- resources: '/resources',
-
- market_commodities: '/markets/commodities',
- market_forex: '/markets/forex',
- market_stock: '/markets/stock',
- market_synthetic: '/markets/synthetic',
- markets: '/markets',
-
- platform_binary_bot: '/platforms/binary-bot',
- platform_dbot: '/platforms/dbot',
- platform_dmt5: '/platforms/dmt5',
- platform_dmt5_financial: '/platforms/dmt5-financial',
- platform_dmt5_financial_stp: '/platforms/dmt5-financial-stp',
- platform_dmt5_synthetic: '/platforms/dmt5-synthetic',
- platform_dtrader: '/platforms/dtrader',
- platform_smarttrader: '/platforms/smarttrader',
- platforms: '/platforms',
-
- trade_type_cfds: '/trade-types/cfds',
- trade_type_multipliers: '/trade-types/multipliers',
- trade_type_options: '/trade-types/options',
- trade_types: '/trade-types',
-
- wallet_bank_wire: '/wallets/bank-wire',
- wallet_cards: '/wallets/cards',
- wallet_crypto: '/wallets/crypto',
- wallet_ewallet: '/wallets/ewallet',
- wallets: '/wallets',
- };
-
- public setConfig(config: TConfigProps): void {
- this.has_router = config.has_router;
- this.routes = config.routes;
- }
-}
diff --git a/packages/appstore/src/stores/index.ts b/packages/appstore/src/stores/index.ts
index 7f15b07398b9..86259b611fcd 100644
--- a/packages/appstore/src/stores/index.ts
+++ b/packages/appstore/src/stores/index.ts
@@ -1,18 +1,4 @@
-import * as React from 'react';
-import { initWs } from 'Services/websocket';
-import { TRootStore } from 'Types';
-import RootStore from './root-store';
-
-let stores_context: React.Context;
-
-export const initContext = (core_store: TRootStore, websocket: Record): void => {
- if (!stores_context) {
- const root_store = new RootStore(core_store);
- stores_context = React.createContext(root_store);
-
- initWs(websocket);
- }
-};
+import { useStore } from '@deriv/stores';
/** @deprecated Use `useStore` from `@deriv/stores` package instead. */
-export const useStores = (): TRootStore => React.useContext(stores_context);
+export const useStores: () => any = useStore;
diff --git a/packages/appstore/src/stores/root-store.ts b/packages/appstore/src/stores/root-store.ts
deleted file mode 100644
index 471185b72deb..000000000000
--- a/packages/appstore/src/stores/root-store.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { TRootStore } from 'Types';
-import ConfigStore from './config-store';
-
-export default class RootStore {
- public config: ConfigStore;
- public ws: unknown;
- public client: Record;
- public common: Record;
- public ui: Record;
- public modules: Record;
- public notifications: Record;
- public traders_hub: Record;
-
- public constructor(core_store: TRootStore) {
- this.config = new ConfigStore(this);
- this.client = core_store.client;
- this.common = core_store.common;
- this.ui = core_store.ui;
- this.modules = core_store.modules;
- this.notifications = core_store.notifications;
- this.traders_hub = core_store.traders_hub;
- }
-}
diff --git a/packages/appstore/src/types/api.types.ts b/packages/appstore/src/types/api.types.ts
deleted file mode 100644
index 90ebe40a82b9..000000000000
--- a/packages/appstore/src/types/api.types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export type TErrorResponse =
- | {
- message: string;
- code: string;
- }
- | undefined;
diff --git a/packages/appstore/src/types/common.types.ts b/packages/appstore/src/types/common.types.ts
index 846ee2f212c4..7bd4ef4ece1b 100644
--- a/packages/appstore/src/types/common.types.ts
+++ b/packages/appstore/src/types/common.types.ts
@@ -1,4 +1,6 @@
import { DetailsOfEachMT5Loginid } from '@deriv/api-types';
+import { useWalletsList, useAvailableWallets } from '@deriv/hooks';
+import { useStore } from '@deriv/stores';
import { PlatformIcons } from 'Assets/svgs/trading-platform';
import { RegionAvailability } from 'Constants/platform-config';
@@ -134,6 +136,8 @@ export interface AvailableAccount {
market_type?: 'all' | 'financial' | 'synthetic';
icon: keyof typeof PlatformIcons;
availability: RegionAvailability;
+ short_code_and_region?: string;
+ login?: string;
}
export type Currency =
@@ -167,3 +171,73 @@ export interface AccountListDetail {
loginid: string;
title: string;
}
+
+export type TAccountStatus = 'pending' | 'failed' | 'need_verification' | '';
+export type TWalletCurrency =
+ | Extract
+ | 'USDT'
+ | 'eUSDT'
+ | 'tUSDT';
+export type TWalletShortcode = Extract;
+export type TLinkedTo = {
+ loginid?: string;
+ platform?: string;
+ balance?: string;
+ currency?: string;
+};
+
+export type TWalletAccount = NonNullable['data']>[number];
+export type TWalletInfo = NonNullable['data']>[number];
+
+export type TTransferAccount = {
+ active_wallet_icon: string | undefined;
+ account_type?: 'wallet' | 'trading' | 'dxtrade' | 'mt5' | 'derivez' | 'binary' | 'ctrader';
+ balance: number;
+ currency?: string;
+ display_currency_code: string | undefined;
+ gradient_class?: `wallet-card__${string}`;
+ icon?: string;
+ is_demo: boolean;
+ loginid?: string;
+ mt5_market_type?: 'all' | 'financial' | 'synthetic';
+ shortcode: string | undefined;
+ type: 'fiat' | 'crypto' | 'demo';
+};
+
+export type TMessageItem =
+ | {
+ variant: 'base';
+ key: string;
+ type: 'info' | 'error' | 'success';
+ message: string | JSX.Element;
+ }
+ | {
+ variant: 'with-action-button';
+ onClickHandler: VoidFunction;
+ button_label: string;
+ key: string;
+ type: 'info' | 'error' | 'success';
+ message: string | JSX.Element;
+ };
+
+export type TWalletButton = {
+ name: Parameters['traders_hub']['setWalletModalActiveTab']>[0];
+ text: string;
+ icon: string;
+ action: () => void;
+};
+
+export type TWalletSteps = {
+ handleBack: () => void;
+ handleClose: () => void;
+ handleNext: () => void;
+ is_disabled: boolean;
+ toggleCheckbox: () => void;
+ upgradeToWallets: (value: boolean) => void;
+};
+
+export type TRealWalletsUpgradeSteps = {
+ wallet_upgrade_steps: TWalletSteps & {
+ current_step: number;
+ };
+};
diff --git a/packages/appstore/src/types/index.ts b/packages/appstore/src/types/index.ts
index edc109feef38..237db8bfebe2 100644
--- a/packages/appstore/src/types/index.ts
+++ b/packages/appstore/src/types/index.ts
@@ -1,6 +1 @@
-export * from './props.types';
-export * from './stores.types';
-export * from './params.types';
-export * from './api.types';
-export * from './ws.types';
export * from './common.types';
diff --git a/packages/appstore/src/types/params.types.ts b/packages/appstore/src/types/params.types.ts
deleted file mode 100644
index 182bbc23e79d..000000000000
--- a/packages/appstore/src/types/params.types.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-export type TRoute = {
- component?: any;
- default?: boolean;
- exact?: boolean;
- getTitle?: () => string;
- icon?: string;
- is_authenticated?: boolean;
- is_routed?: boolean;
- is_modal?: boolean;
- label?: string;
- path?: string;
- routes?: TRoute[];
- subroutes?: TRoute[];
- to?: string;
- is_logged_in?: boolean;
- is_logging_in?: boolean;
-};
-
-export type TRouteGroup = {
- default?: boolean;
- icon?: string;
- getTitle?: () => string;
- path?: string;
- subitems?: number[];
-};
-export type TRouteConfig = TRoute & {
- is_modal?: boolean;
- is_authenticated?: boolean;
- routes?: TRoute[];
-};
diff --git a/packages/appstore/src/types/props.types.ts b/packages/appstore/src/types/props.types.ts
deleted file mode 100644
index ed94bb03b11d..000000000000
--- a/packages/appstore/src/types/props.types.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import ConfigStore from 'Stores/config-store';
-
-export type TConfigProps = {
- has_router: boolean;
- routes: ConfigStore['routes'];
-};
diff --git a/packages/appstore/src/types/stores.types.ts b/packages/appstore/src/types/stores.types.ts
deleted file mode 100644
index 690863da4f7e..000000000000
--- a/packages/appstore/src/types/stores.types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import ConfigStore from 'Stores/config-store';
-
-export type TRootStore = {
- ui: Record;
- common: Record;
- client: Record;
- config: ConfigStore;
- modules: Record;
- notifications: Record;
- traders_hub: Record;
-};
diff --git a/packages/appstore/src/types/ws.types.ts b/packages/appstore/src/types/ws.types.ts
deleted file mode 100644
index 8dc541ad5384..000000000000
--- a/packages/appstore/src/types/ws.types.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export type DerivWS = any;
diff --git a/packages/appstore/webpack.config.js b/packages/appstore/webpack.config.js
index 2326f6fd28df..ea6310164228 100644
--- a/packages/appstore/webpack.config.js
+++ b/packages/appstore/webpack.config.js
@@ -107,9 +107,6 @@ module.exports = function (env) {
'style-loader',
{
loader: 'css-loader',
- options: {
- url: (_, resourcePath) => resourcePath.includes('packages/wallets'),
- },
},
{
loader: 'postcss-loader',
@@ -158,6 +155,15 @@ module.exports = function (env) {
include: /public\//,
use: svg_loaders,
},
+ {
+ test: /\.svg$/,
+ exclude: [/node_modules/, path.resolve('../', 'wallets')],
+ include: /public\//,
+ type: 'asset/resource',
+ generator: {
+ filename: 'appstore/public/[name].[contenthash][ext]',
+ },
+ },
{
test: /\.svg$/,
exclude: [/node_modules|public\//],
diff --git a/packages/bot-skeleton/package.json b/packages/bot-skeleton/package.json
index f396b5a390ac..afcdb7aea322 100644
--- a/packages/bot-skeleton/package.json
+++ b/packages/bot-skeleton/package.json
@@ -35,7 +35,7 @@
"eslint-plugin-react-hooks": "^4.2.0"
},
"dependencies": {
- "@deriv/deriv-api": "^1.0.11",
+ "@deriv/deriv-api": "^1.0.13",
"@deriv/indicators": "^1.0.0",
"@deriv/js-interpreter": "^3.0.0",
"@deriv/shared": "^1.0.0",
diff --git a/packages/bot-skeleton/src/scratch/dbot.js b/packages/bot-skeleton/src/scratch/dbot.js
index 0eadcfcc3f29..312aef9128ef 100644
--- a/packages/bot-skeleton/src/scratch/dbot.js
+++ b/packages/bot-skeleton/src/scratch/dbot.js
@@ -1,14 +1,16 @@
-import './blockly';
-import { isAllRequiredBlocksEnabled, updateDisabledBlocks, validateErrorOnBlockDelete } from './utils';
-import main_xml from './xml/main.xml';
-import DBotStore from './dbot-store';
import { save_types } from '../constants';
import { config } from '../constants/config';
-import { getSavedWorkspaces, saveWorkspaceToRecent } from '../utils/local-storage';
-import { observer as globalObserver, compareXml } from '../utils';
+import { api_base } from '../services/api/api-base';
import ApiHelpers from '../services/api/api-helpers';
import Interpreter from '../services/tradeEngine/utils/interpreter';
-import { api_base } from '../services/api/api-base';
+import { compareXml,observer as globalObserver } from '../utils';
+import { getSavedWorkspaces, saveWorkspaceToRecent } from '../utils/local-storage';
+
+import main_xml from './xml/main.xml';
+import DBotStore from './dbot-store';
+import { isAllRequiredBlocksEnabled, updateDisabledBlocks, validateErrorOnBlockDelete } from './utils';
+
+import './blockly';
class DBot {
constructor() {
diff --git a/packages/bot-web-ui/package-lock.json b/packages/bot-web-ui/package-lock.json
index dd2b96df9265..d67f84ddb208 100644
--- a/packages/bot-web-ui/package-lock.json
+++ b/packages/bot-web-ui/package-lock.json
@@ -95,7 +95,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
- "@deriv/deriv-api": "^1.0.11",
+ "@deriv/deriv-api": "^1.0.13",
"@deriv/indicators": "^1.0.0",
"@deriv/js-interpreter": "^3.0.0",
"@deriv/shared": "^1.0.0",
diff --git a/packages/bot-web-ui/src/components/dashboard/bot-builder/bot-builder.tsx b/packages/bot-web-ui/src/components/dashboard/bot-builder/bot-builder.tsx
index a7bfbcde7dcd..3caffd8258e4 100644
--- a/packages/bot-web-ui/src/components/dashboard/bot-builder/bot-builder.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/bot-builder/bot-builder.tsx
@@ -1,11 +1,14 @@
import React from 'react';
import classNames from 'classnames';
+
import { observer } from '@deriv/stores';
+
import { useDBotStore } from '../../../stores/useDBotStore';
import LoadModal from '../../load-modal';
import SaveModal from '../dashboard-component/load-bot-preview/save-modal';
import BotBuilderTourHandler from '../dbot-tours/bot-builder-tour';
import QuickStrategy from '../quick-strategy';
+
import WorkspaceWrapper from './workspace-wrapper';
const BotBuilder = observer(() => {
diff --git a/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/__tests__/toolbar.spec.tsx b/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/__tests__/toolbar.spec.tsx
index 4110c8d34bdf..3a4028460580 100644
--- a/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/__tests__/toolbar.spec.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/__tests__/toolbar.spec.tsx
@@ -1,5 +1,6 @@
import React from 'react';
-import { isDesktop, isMobile } from '@deriv/shared';
+import { useStore } from '@deriv/stores';
+// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen } from '@testing-library/react';
import { useDBotStore } from 'Stores/useDBotStore';
import Toolbar from '..';
@@ -33,10 +34,14 @@ jest.mock('Stores/useDBotStore', () => ({
useDBotStore: jest.fn(() => mockDbotStore),
}));
-jest.mock('@deriv/shared', () => ({
- ...jest.requireActual('@deriv/shared'),
- isMobile: jest.fn(() => false),
- isDesktop: jest.fn(() => true),
+jest.mock('@deriv/stores', () => ({
+ ...jest.requireActual('@deriv/stores'),
+ observer: jest.fn(x => x),
+ useStore: jest.fn(() => ({
+ ui: {
+ is_mobile: false,
+ },
+ })),
}));
describe('Toolbar component', () => {
@@ -61,8 +66,11 @@ describe('Toolbar component', () => {
});
it('Toolbar should renders a button, when it is mobile version', async () => {
- (isDesktop as jest.Mock).mockReturnValue(false);
- (isMobile as jest.Mock).mockReturnValue(true);
+ (useStore as jest.Mock).mockReturnValue({
+ ui: {
+ is_mobile: true,
+ },
+ });
render();
expect(await screen.findByRole('button')).toBeInTheDocument();
});
diff --git a/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/toolbar.tsx b/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/toolbar.tsx
index 4b1fcd1b3141..e2eb70bbe3b6 100644
--- a/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/toolbar.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/bot-builder/toolbar/toolbar.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { Dialog } from '@deriv/components';
-import { isMobile } from '@deriv/shared';
-import { observer } from '@deriv/stores';
+import { observer, useStore } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
import { useDBotStore } from 'Stores/useDBotStore';
import ToolbarButton from './toolbar-button';
@@ -9,6 +8,9 @@ import WorkspaceGroup from './workspace-group';
const Toolbar = observer(() => {
const { run_panel, save_modal, load_modal, toolbar, quick_strategy } = useDBotStore();
+ const {
+ ui: { is_mobile },
+ } = useStore();
const {
has_redo_stack,
has_undo_stack,
@@ -32,7 +34,7 @@ const Toolbar = observer(() => {
- {isMobile() && (
+ {is_mobile && (
{
]}
/>
) : (
- localize('Any unsaved changes will be lost.')
+
)}
diff --git a/packages/bot-web-ui/src/components/dashboard/dashboard-component/__tests__/dashboard-component.spec.tsx b/packages/bot-web-ui/src/components/dashboard/dashboard-component/__tests__/dashboard-component.spec.tsx
index 9e904e5bfa07..7dac2d6e335e 100644
--- a/packages/bot-web-ui/src/components/dashboard/dashboard-component/__tests__/dashboard-component.spec.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dashboard-component/__tests__/dashboard-component.spec.tsx
@@ -1,7 +1,9 @@
import React from 'react';
+
import { isMobile } from '@deriv/shared';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+
import UserGuide from '../user-guide';
jest.mock('@deriv/components', () => {
diff --git a/packages/bot-web-ui/src/components/dashboard/dashboard-component/dashboard-component.tsx b/packages/bot-web-ui/src/components/dashboard/dashboard-component/dashboard-component.tsx
index 0786c435beb1..977df2d29739 100644
--- a/packages/bot-web-ui/src/components/dashboard/dashboard-component/dashboard-component.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dashboard-component/dashboard-component.tsx
@@ -1,11 +1,15 @@
import React from 'react';
import classNames from 'classnames';
+
import { DesktopWrapper, MobileWrapper, Text } from '@deriv/components';
import { isMobile } from '@deriv/shared';
import { observer } from '@deriv/stores';
import { localize } from '@deriv/translations';
+
import { useDBotStore } from 'Stores/useDBotStore';
+
import OnboardTourHandler from '../dbot-tours/onboarding-tour';
+
import Local from './load-bot-preview/local';
import Cards from './cards';
import InfoPanel from './info-panel';
diff --git a/packages/bot-web-ui/src/components/dashboard/dashboard-component/info-panel.tsx b/packages/bot-web-ui/src/components/dashboard/dashboard-component/info-panel.tsx
index 04bca7895733..06c29f16119a 100644
--- a/packages/bot-web-ui/src/components/dashboard/dashboard-component/info-panel.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dashboard-component/info-panel.tsx
@@ -1,14 +1,18 @@
import React from 'react';
import classNames from 'classnames';
+
import { DesktopWrapper, Icon, MobileWrapper, Modal, Text } from '@deriv/components';
-import { isMobile } from '@deriv/shared';
-import { observer } from '@deriv/stores';
+import { observer, useStore } from '@deriv/stores';
+
import { DBOT_TABS } from 'Constants/bot-contents';
import { useDBotStore } from 'Stores/useDBotStore';
+
import { SIDEBAR_INTRO } from './constants';
const InfoPanel = observer(() => {
- const is_mobile = isMobile();
+ const {
+ ui: { is_mobile },
+ } = useStore();
const { dashboard } = useDBotStore();
const { active_tour, is_info_panel_visible, setActiveTab, setActiveTabTutorial, setInfoPanelVisibility } =
dashboard;
diff --git a/packages/bot-web-ui/src/components/dashboard/dashboard-component/load-bot-preview/recent-workspace.tsx b/packages/bot-web-ui/src/components/dashboard/dashboard-component/load-bot-preview/recent-workspace.tsx
index 3380df771da9..b5efd9be7dc2 100644
--- a/packages/bot-web-ui/src/components/dashboard/dashboard-component/load-bot-preview/recent-workspace.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dashboard-component/load-bot-preview/recent-workspace.tsx
@@ -1,15 +1,20 @@
import React from 'react';
import classnames from 'classnames';
+
import { timeSince } from '@deriv/bot-skeleton';
import { save_types } from '@deriv/bot-skeleton/src/constants/save-type';
import { DesktopWrapper, Icon, MobileWrapper, Text } from '@deriv/components';
import { isDesktop, isMobile } from '@deriv/shared';
import { observer } from '@deriv/stores';
+
import { DBOT_TABS } from 'Constants/bot-contents';
-import { waitForDomElement } from 'Utils/dom-observer';
import { useDBotStore } from 'Stores/useDBotStore';
+import { waitForDomElement } from 'Utils/dom-observer';
+
import { useComponentVisibility } from '../../hooks/useComponentVisibility';
+
import { CONTEXT_MENU_MOBILE, MENU_DESKTOP, STRATEGY } from './constants';
+
import './index.scss';
type TRecentWorkspace = {
diff --git a/packages/bot-web-ui/src/components/dashboard/dashboard.tsx b/packages/bot-web-ui/src/components/dashboard/dashboard.tsx
index 8990bb53c772..2fe93bc328d7 100644
--- a/packages/bot-web-ui/src/components/dashboard/dashboard.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dashboard.tsx
@@ -1,5 +1,6 @@
import React, { useEffect } from 'react';
import classNames from 'classnames';
+
import { updateWorkspaceName } from '@deriv/bot-skeleton';
import dbot from '@deriv/bot-skeleton/src/scratch/dbot';
import { initTrashCan } from '@deriv/bot-skeleton/src/scratch/hooks/trashcan';
@@ -8,10 +9,13 @@ import { DesktopWrapper, Dialog, MobileWrapper, Tabs } from '@deriv/components';
import { isMobile } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { localize } from '@deriv/translations';
+
import Chart from 'Components/chart';
import { DBOT_TABS, TAB_IDS } from 'Constants/bot-contents';
import { useDBotStore } from 'Stores/useDBotStore';
+
import RunPanel from '../run-panel';
+
import RunStrategy from './dashboard-component/run-strategy';
import { tour_list } from './dbot-tours/utils';
import DashboardComponent from './dashboard-component';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-desktop.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-desktop.tsx
index 102d06d91b0c..9d0044dc21c7 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-desktop.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-desktop.tsx
@@ -1,7 +1,10 @@
import React from 'react';
+
import { observer } from '@deriv/stores';
-import { getSetting } from 'Utils/settings';
+
import { useDBotStore } from 'Stores/useDBotStore';
+import { getSetting } from 'Utils/settings';
+
import ReactJoyrideWrapper from '../common/react-joyride-wrapper';
import TourEndDialog from '../common/tour-end-dialog';
import TourStartDialog from '../common/tour-start-dialog';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-mobile.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-mobile.tsx
index 1964f56f0b39..fb11296b7fe5 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-mobile.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/bot-builder-tour-mobile.tsx
@@ -1,9 +1,12 @@
import React from 'react';
+
import { ProgressBarTracker } from '@deriv/components';
import { observer } from '@deriv/stores';
import { localize } from '@deriv/translations';
-import { getSetting } from 'Utils/settings';
+
import { useDBotStore } from 'Stores/useDBotStore';
+import { getSetting } from 'Utils/settings';
+
import Accordion from '../common/accordion';
import TourButton from '../common/tour-button';
import TourStartDialog from '../common/tour-start-dialog';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/index.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/index.tsx
index d8208c9cba57..3b512e67d903 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/index.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/bot-builder-tour/index.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import { observer } from 'mobx-react';
+
import { isMobile } from '@deriv/shared';
+
import BotBuilderTourDesktop from './bot-builder-tour-desktop';
import BotBuilderTourMobile from './bot-builder-tour-mobile';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/accordion.spec.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/accordion.spec.tsx
index 2577b3b9fb2d..699f1850fc7e 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/accordion.spec.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/accordion.spec.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
+
import Accordion from '../accordion';
const mocked_props = {
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/react-joyride-wrapper.spec.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/react-joyride-wrapper.spec.tsx
index d7a58b2f8150..7e71f25c1fd2 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/react-joyride-wrapper.spec.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/react-joyride-wrapper.spec.tsx
@@ -1,9 +1,12 @@
import React from 'react';
+
import { mockStore, StoreProvider } from '@deriv/stores';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen } from '@testing-library/react';
-import { mock_ws } from 'Utils/mock';
+
import { DBotStoreProvider, mockDBotStore } from 'Stores/useDBotStore';
+import { mock_ws } from 'Utils/mock';
+
import ReactJoyrideWrapper from '../react-joyride-wrapper';
jest.mock('@deriv/bot-skeleton/src/scratch/blockly', () => jest.fn());
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/tour-button.spec.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/tour-button.spec.tsx
index 9cd8e41ccd4b..960e55f7bdf8 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/tour-button.spec.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/__tests__/tour-button.spec.tsx
@@ -1,5 +1,7 @@
import React from 'react';
+
import { fireEvent, render, screen } from '@testing-library/react';
+
import TourButton from '../tour-button';
const mocked_props = {
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/accordion.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/accordion.tsx
index 02da2ffa2940..797ba949058b 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/accordion.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/accordion.tsx
@@ -1,7 +1,9 @@
import React from 'react';
import classNames from 'classnames';
+
import { Icon, Text } from '@deriv/components';
import { localize } from '@deriv/translations';
+
import { TStepMobile } from '../config';
type TAccordion = {
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/react-joyride-wrapper.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/react-joyride-wrapper.tsx
index 103226c2e470..a123f715050d 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/react-joyride-wrapper.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/react-joyride-wrapper.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import ReactJoyride, { CallBackProps, Step, Styles } from 'react-joyride';
+
import { localize } from '@deriv/translations';
const common_tour_button_properties = {
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-button.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-button.tsx
index c1d92380adef..fca1280afd76 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-button.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-button.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+
import { Text } from '@deriv/components';
type TTourButton = {
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-end-dialog.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-end-dialog.tsx
index f5c273613509..aeeae1c010db 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-end-dialog.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-end-dialog.tsx
@@ -1,8 +1,10 @@
import React from 'react';
+
import { Dialog, Text } from '@deriv/components';
import { isMobile } from '@deriv/shared';
import { observer } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
+
import { useDBotStore } from '../../../../stores/useDBotStore';
const TourEndDialog = observer(() => {
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-start-dialog.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-start-dialog.tsx
index 673936b6ba67..27983aebffbc 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-start-dialog.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-start-dialog.tsx
@@ -1,9 +1,12 @@
import React from 'react';
+
import { Dialog, Text } from '@deriv/components';
import { isMobile } from '@deriv/shared';
import { observer } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
+
import { DBOT_TABS } from 'Constants/bot-contents';
+
import { useDBotStore } from '../../../../stores/useDBotStore';
import { bot_builder_tour_header, onboarding_tour_header, tourDialogAction, tourDialogInfo } from '../config';
import { setTourSettings, tour_list } from '../utils';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-steps.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-steps.tsx
index da7f22c27029..42dcd3da26bb 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-steps.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/common/tour-steps.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+
import { getUUID } from '@deriv/bot-skeleton/src/services/tradeEngine/utils/helpers';
import { Text } from '@deriv/components';
import { observer } from '@deriv/stores';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/__tests__/config.spec.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/__tests__/config.spec.tsx
index 6740ebc9fdd8..7981e387a27d 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/__tests__/config.spec.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/__tests__/config.spec.tsx
@@ -1,9 +1,12 @@
import React from 'react';
+
import { mockStore, StoreProvider } from '@deriv/stores';
import { render, screen } from '@testing-library/react';
-import { mock_ws } from 'Utils/mock';
+
import RootStore from 'Stores/root-store';
import { DBotStoreProvider, mockDBotStore } from 'Stores/useDBotStore';
+import { mock_ws } from 'Utils/mock';
+
import OnboardingTourMobile from '../../onboarding-tour/onboarding-tour-mobile';
import { DBOT_ONBOARDING_MOBILE } from '../index';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/index.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/index.tsx
index 6dd3f126ef03..b80ed86103f2 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/index.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/config/index.tsx
@@ -1,8 +1,10 @@
-import { getImageLocation } from '../../../../public-path';
import React from 'react';
+
import { Icon, Text } from '@deriv/components';
import { getUrlBase, isMobile } from '@deriv/shared';
import { Localize, localize } from '@deriv/translations';
+
+import { getImageLocation } from '../../../../public-path';
import TourSteps from '../common/tour-steps';
const is_mobile = isMobile();
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/index.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/index.tsx
index b27e470f5ba6..7818f82e5e23 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/index.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/index.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import { observer } from 'mobx-react';
+
import { isMobile } from '@deriv/shared';
+
import OnboardingTourDesktop from './onboarding-tour-desktop';
import OnboardingTourMobile from './onboarding-tour-mobile';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-desktop.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-desktop.tsx
index c77788a34df6..528a84d0def7 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-desktop.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-desktop.tsx
@@ -1,7 +1,10 @@
import React from 'react';
+
import { observer } from '@deriv/stores';
-import { getSetting } from 'Utils/settings';
+
import { useDBotStore } from 'Stores/useDBotStore';
+import { getSetting } from 'Utils/settings';
+
import ReactJoyrideWrapper from '../common/react-joyride-wrapper';
import TourStartDialog from '../common/tour-start-dialog';
import { DBOT_ONBOARDING } from '../config';
diff --git a/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-mobile.tsx b/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-mobile.tsx
index 2e04b3e49234..b9454cab4784 100644
--- a/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-mobile.tsx
+++ b/packages/bot-web-ui/src/components/dashboard/dbot-tours/onboarding-tour/onboarding-tour-mobile.tsx
@@ -1,10 +1,13 @@
import React from 'react';
import classNames from 'classnames';
+
import { Icon, ProgressBarTracker, Text } from '@deriv/components';
import { observer } from '@deriv/stores';
import { localize } from '@deriv/translations';
-import { getSetting } from 'Utils/settings';
+
import { useDBotStore } from 'Stores/useDBotStore';
+import { getSetting } from 'Utils/settings';
+
import TourButton from '../common/tour-button';
import { DBOT_ONBOARDING_MOBILE, TMobileTourConfig } from '../config';
diff --git a/packages/bot-web-ui/src/components/load-modal/local.tsx b/packages/bot-web-ui/src/components/load-modal/local.tsx
index 005fd0372a14..446ae77b5193 100644
--- a/packages/bot-web-ui/src/components/load-modal/local.tsx
+++ b/packages/bot-web-ui/src/components/load-modal/local.tsx
@@ -1,10 +1,13 @@
import React from 'react';
import classNames from 'classnames';
+
import { Button, Icon } from '@deriv/components';
import { isMobile } from '@deriv/shared';
import { observer } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
+
import { useDBotStore } from 'Stores/useDBotStore';
+
import LocalFooter from './local-footer';
import WorkspaceControl from './workspace-control';
diff --git a/packages/bot-web-ui/src/components/run-panel/run-panel.tsx b/packages/bot-web-ui/src/components/run-panel/run-panel.tsx
index b0ef95e8231f..d49bb69d5efc 100644
--- a/packages/bot-web-ui/src/components/run-panel/run-panel.tsx
+++ b/packages/bot-web-ui/src/components/run-panel/run-panel.tsx
@@ -1,9 +1,10 @@
import React from 'react';
import classNames from 'classnames';
+
import { Button, Drawer, Modal, Money, Tabs, Text, ThemedScrollbars } from '@deriv/components';
-import { isMobile } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
+
import Journal from 'Components/journal';
import SelfExclusion from 'Components/self-exclusion';
import Summary from 'Components/summary';
@@ -230,7 +231,10 @@ const StatisticsInfoModal = ({
const RunPanel = observer(() => {
const { run_panel, dashboard } = useDBotStore();
- const { client } = useStore();
+ const {
+ client,
+ ui: { is_mobile },
+ } = useStore();
const { currency } = client;
const {
active_index,
@@ -250,8 +254,6 @@ const RunPanel = observer(() => {
const { total_payout, total_profit, total_stake, won_contracts, lost_contracts, number_of_runs } = statistics;
const { BOT_BUILDER, CHART } = DBOT_TABS;
- const is_mobile = isMobile();
-
React.useEffect(() => {
onMount();
return () => onUnmount();
diff --git a/packages/bot-web-ui/src/components/summary/summary-card.tsx b/packages/bot-web-ui/src/components/summary/summary-card.tsx
index 4ef10938d3f4..c061587ea502 100644
--- a/packages/bot-web-ui/src/components/summary/summary-card.tsx
+++ b/packages/bot-web-ui/src/components/summary/summary-card.tsx
@@ -1,12 +1,15 @@
import React from 'react';
import classNames from 'classnames';
+
import { ContractCard, Text } from '@deriv/components';
import { getCardLabels, isMobile } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { localize } from '@deriv/translations';
+
import ContractCardLoader from 'Components/contract-card-loading';
import { getContractTypeDisplay } from 'Constants/contract';
import { useDBotStore } from 'Stores/useDBotStore';
+
import { TSummaryCardProps } from './summary-card.types';
const SummaryCard = observer(({ contract_info, is_contract_loading }: TSummaryCardProps) => {
diff --git a/packages/bot-web-ui/src/components/transaction-details/__tests__/transaction-details.spec.tsx b/packages/bot-web-ui/src/components/transaction-details/__tests__/transaction-details.spec.tsx
index c0c426728fc2..67102dcb8d90 100644
--- a/packages/bot-web-ui/src/components/transaction-details/__tests__/transaction-details.spec.tsx
+++ b/packages/bot-web-ui/src/components/transaction-details/__tests__/transaction-details.spec.tsx
@@ -1,17 +1,10 @@
import React from 'react';
-import { isMobile } from '@deriv/shared';
import { mockStore, StoreProvider } from '@deriv/stores';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen, waitFor } from '@testing-library/react';
-import RootStore from 'Stores/index';
import { DBotStoreProvider, mockDBotStore } from 'Stores/useDBotStore';
import TransactionDetails from '../transaction-details';
-jest.mock('@deriv/shared', () => ({
- ...jest.requireActual('@deriv/shared'),
- isMobile: jest.fn(),
-}));
-
jest.mock('@deriv/bot-skeleton/src/scratch/blockly', () => jest.fn());
jest.mock('@deriv/bot-skeleton/src/scratch/dbot', () => ({
saveRecentWorkspace: jest.fn(),
@@ -47,31 +40,37 @@ const mock_ws = {
send: jest.fn(),
};
describe('TransactionDetails', () => {
- let wrapper: ({ children }: { children: JSX.Element }) => JSX.Element, mock_DBot_store: RootStore | undefined;
-
beforeEach(() => {
jest.resetModules();
- const mock_store = mockStore({});
- mock_DBot_store = mockDBotStore(mock_store, mock_ws);
+ });
- wrapper = ({ children }: { children: JSX.Element }) => (
+ const wrapper = (mock_store: ReturnType) => {
+ const mock_DBot_store = mockDBotStore(mock_store, mock_ws);
+
+ const Component = ({ children }: { children: JSX.Element }) => (
{children}
);
- });
+
+ return Component;
+ };
it('should render Desktop component based on Desktop', async () => {
- (isMobile as jest.Mock).mockReturnValueOnce(false);
- await waitFor(() => render(, { wrapper }));
+ const mock_store = mockStore({});
+ await waitFor(() => render(, { wrapper: wrapper(mock_store) }));
expect(screen.queryByText('Desktop Details')).toBeInTheDocument();
});
it('should render Mobile component on mobile', async () => {
- (isMobile as jest.Mock).mockReturnValueOnce(true);
- await waitFor(() => render(, { wrapper }));
+ const mock_store = mockStore({
+ ui: {
+ is_mobile: true,
+ },
+ });
+ await waitFor(() => render(, { wrapper: wrapper(mock_store) }));
expect(screen.getByText('Mobile Details')).toBeInTheDocument();
});
});
diff --git a/packages/bot-web-ui/src/components/transaction-details/transaction-details-mobile.tsx b/packages/bot-web-ui/src/components/transaction-details/transaction-details-mobile.tsx
index b84c6ffc6bb0..6a14171ad93e 100644
--- a/packages/bot-web-ui/src/components/transaction-details/transaction-details-mobile.tsx
+++ b/packages/bot-web-ui/src/components/transaction-details/transaction-details-mobile.tsx
@@ -1,12 +1,16 @@
import React from 'react';
+
import { MobileFullPageModal } from '@deriv/components';
import { observer, useStore } from '@deriv/stores';
import { localize } from '@deriv/translations';
+
import { StatisticsSummary } from 'Components/run-panel/run-panel';
import { transaction_elements } from 'Constants/transactions';
import { useDBotStore } from 'Stores/useDBotStore';
+
import MobileTransactionCards from './mobile-transaction-card';
import { TRunPanelStore, TTransactionStore } from './transaction-details.types';
+
import './transaction-details-mobile.scss';
const TransactionDetailsMobile = observer(() => {
diff --git a/packages/bot-web-ui/src/components/transaction-details/transaction-details.tsx b/packages/bot-web-ui/src/components/transaction-details/transaction-details.tsx
index 432682da3fe1..dc809a71f8a4 100644
--- a/packages/bot-web-ui/src/components/transaction-details/transaction-details.tsx
+++ b/packages/bot-web-ui/src/components/transaction-details/transaction-details.tsx
@@ -1,14 +1,18 @@
import React, { Suspense } from 'react';
import { Loading } from '@deriv/components';
-import { isMobile } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
import TransactionDetailsDesktop from './transaction-details-desktop';
import TransactionDetailsMobile from './transaction-details-mobile';
-export default function TransactionDetails() {
- const is_mobile = isMobile();
+export const TransactionDetails = observer(() => {
+ const {
+ ui: { is_mobile },
+ } = useStore();
return (
}>
{is_mobile ? : }
);
-}
+});
+
+export default TransactionDetails;
diff --git a/packages/bot-web-ui/src/constants/contract.js b/packages/bot-web-ui/src/constants/contract.js
index 0efd631e0205..44a18458b511 100644
--- a/packages/bot-web-ui/src/constants/contract.js
+++ b/packages/bot-web-ui/src/constants/contract.js
@@ -1,5 +1,6 @@
import { getTotalProfit } from '@deriv/shared';
import { localize } from '@deriv/translations';
+
import { getBuyPrice } from 'Utils/multiplier';
export const getSupportedContracts = is_high_low => ({
diff --git a/packages/bot-web-ui/src/stores/dashboard-store.ts b/packages/bot-web-ui/src/stores/dashboard-store.ts
index b04a4f1b0fa5..3bf83b1e15e0 100644
--- a/packages/bot-web-ui/src/stores/dashboard-store.ts
+++ b/packages/bot-web-ui/src/stores/dashboard-store.ts
@@ -1,8 +1,12 @@
import { action, computed, makeObservable, observable, reaction } from 'mobx';
+
import { setColors } from '@deriv/bot-skeleton';
import { isMobile } from '@deriv/shared';
+
import { clearInjectionDiv } from 'Constants/load-modal';
+
import { setTourSettings, tour_type, TTourType } from '../components/dashboard/dbot-tours/utils';
+
import RootStore from './root-store';
export interface IDashboardStore {
diff --git a/packages/bot-web-ui/src/stores/download-store.js b/packages/bot-web-ui/src/stores/download-store.js
index 5c60ffc34ce5..582efa895531 100644
--- a/packages/bot-web-ui/src/stores/download-store.js
+++ b/packages/bot-web-ui/src/stores/download-store.js
@@ -1,4 +1,5 @@
import { action, makeObservable } from 'mobx';
+
import { log_types } from '@deriv/bot-skeleton';
import { localize } from '@deriv/translations';
diff --git a/packages/bot-web-ui/src/stores/journal-store.js b/packages/bot-web-ui/src/stores/journal-store.js
index 9522b41f735a..6f4d61b5599e 100644
--- a/packages/bot-web-ui/src/stores/journal-store.js
+++ b/packages/bot-web-ui/src/stores/journal-store.js
@@ -1,8 +1,10 @@
import { action, computed, makeObservable, observable, reaction, when } from 'mobx';
+
import { log_types, message_types } from '@deriv/bot-skeleton';
import { config } from '@deriv/bot-skeleton/src/constants/config';
import { formatDate } from '@deriv/shared';
import { localize } from '@deriv/translations';
+
import { isCustomJournalMessage } from '../utils/journal-notifications';
import { getStoredItemsByKey, getStoredItemsByUser, setStoredItemsByKey } from '../utils/session-storage';
import { getSetting, storeSetting } from '../utils/settings';
diff --git a/packages/bot-web-ui/src/stores/summary-card-store.js b/packages/bot-web-ui/src/stores/summary-card-store.js
index 95a414834f61..69c28aa0c318 100644
--- a/packages/bot-web-ui/src/stores/summary-card-store.js
+++ b/packages/bot-web-ui/src/stores/summary-card-store.js
@@ -1,5 +1,7 @@
import { action, computed, makeObservable, observable, reaction } from 'mobx';
+
import { getIndicativePrice, isEqualObject, isMultiplierContract, Validator } from '@deriv/shared';
+
import { getValidationRules } from 'Constants/contract';
import { contract_stages } from 'Constants/contract-stage';
import { getContractUpdateConfig } from 'Utils/multiplier';
diff --git a/packages/cashier/package.json b/packages/cashier/package.json
index 26cbec245694..b470ee74ddc9 100644
--- a/packages/cashier/package.json
+++ b/packages/cashier/package.json
@@ -38,7 +38,7 @@
"@deriv/api": "^1.0.0",
"@deriv/api-types": "^1.0.118",
"@deriv/components": "^1.0.0",
- "@deriv/deriv-api": "^1.0.11",
+ "@deriv/deriv-api": "^1.0.13",
"@deriv/hooks": "^1.0.0",
"@deriv/p2p": "^0.7.3",
"@deriv/shared": "^1.0.0",
diff --git a/packages/cashier/src/components/crypto-fiat-converter/crypto-fiat-converter.scss b/packages/cashier/src/components/crypto-fiat-converter/crypto-fiat-converter.scss
index 9da413fc79a2..305250d49e66 100644
--- a/packages/cashier/src/components/crypto-fiat-converter/crypto-fiat-converter.scss
+++ b/packages/cashier/src/components/crypto-fiat-converter/crypto-fiat-converter.scss
@@ -2,9 +2,11 @@
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-gap: 0.8rem;
+
@include desktop {
max-width: 36rem;
- margin: 0 auto 2.4rem;
+ margin: 0 auto;
+
.dc-icon {
margin-top: 1rem;
}
@@ -12,7 +14,12 @@
.dc-input {
margin-bottom: unset;
}
+
+ .dc-input__hint {
+ margin: 0.1rem 0 0 1.3rem;
+ }
}
+
@include mobile {
display: flex;
flex-direction: column;
@@ -28,6 +35,7 @@
margin: 1.4rem;
}
}
+
.input-group {
display: flex;
@@ -36,6 +44,7 @@
margin-left: -4rem;
}
}
+
&__hint {
grid-column: 3;
margin-left: 0;
diff --git a/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.scss b/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.scss
index 775cb2a2689b..565ebcd856b7 100644
--- a/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.scss
+++ b/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.scss
@@ -1,8 +1,11 @@
.email-verification-empty-state {
display: flex;
- flex: 1;
flex-direction: column;
align-items: center;
justify-content: center;
- margin: 4rem auto 0;
+ gap: 4.6rem;
+
+ @include mobile {
+ gap: 2.4rem;
+ }
}
diff --git a/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.tsx b/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.tsx
index e73862177b55..f8c592a3db46 100644
--- a/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.tsx
+++ b/packages/cashier/src/components/email-verification-empty-state/email-verification-empty-state.tsx
@@ -21,7 +21,7 @@ const EmailVerificationEmptyState = ({ type }: TEmailVerificationEmptyStateProps
return (
{
- if (!isMobile() || (is_default_route && (is_loading || is_cashier_onboarding))) return localize('Cashier');
+ if (!is_mobile || (is_default_route && (is_loading || is_cashier_onboarding))) return localize('Cashier');
return selected_route.getTitle?.();
- }, [is_cashier_onboarding, is_default_route, is_loading, selected_route]);
+ }, [is_cashier_onboarding, is_default_route, is_loading, selected_route, is_mobile]);
const updateActiveTab = useCallback(
(path?: string) => {
@@ -205,7 +205,7 @@ const Cashier = observer(({ history, location, routes: routes_config }: TCashier
}, [history, is_p2p_enabled, is_p2p_enabled_success]);
if (
- ((!is_logged_in || isMobile()) && is_logging_in) ||
+ ((!is_logged_in || is_mobile) && is_logging_in) ||
!is_account_setting_loaded ||
is_payment_agent_checking ||
is_p2p_enabled_loading
diff --git a/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-fiat-card/cashier-onboarding-fiat-card.tsx b/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-fiat-card/cashier-onboarding-fiat-card.tsx
index e42105c9e09c..e243d2ca0f3b 100644
--- a/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-fiat-card/cashier-onboarding-fiat-card.tsx
+++ b/packages/cashier/src/modules/cashier-onboarding/components/cashier-onboarding-fiat-card/cashier-onboarding-fiat-card.tsx
@@ -8,7 +8,7 @@ import { CashierOnboardingCard } from '../cashier-onboarding-card';
import { CashierOnboardingIconMarquee } from '../cashier-onboarding-icon-marquee';
const icons: React.ComponentProps['icons'] = [
- 'IcWalletCreditDebit',
+ 'IcCashierCreditDebit',
'IcCashierInstantBankTransfer',
'IcCashierEwallet',
'IcCashierLocalPaymentMethods',
diff --git a/packages/cashier/src/pages/account-transfer/account-transfer-form/account-transfer-form.scss b/packages/cashier/src/pages/account-transfer/account-transfer-form/account-transfer-form.scss
index 6301c88c6b02..1e752c01ec31 100644
--- a/packages/cashier/src/pages/account-transfer/account-transfer-form/account-transfer-form.scss
+++ b/packages/cashier/src/pages/account-transfer/account-transfer-form/account-transfer-form.scss
@@ -6,6 +6,7 @@
&__hint {
color: var(--text-general);
margin-top: 0.5rem;
+
&__disabled {
color: var(--text-less-prominent);
}
@@ -23,10 +24,12 @@
margin-right: 0.4rem;
}
}
+
&__crypto {
&--disabled {
pointer-events: none;
}
+
&--percentage-selector {
display: flex;
flex-direction: column;
@@ -34,6 +37,7 @@
margin-bottom: 2.4rem;
}
}
+
&__inline-warning-message {
margin-bottom: 3rem;
@@ -42,6 +46,7 @@
width: 100%;
}
}
+
&__drop-down-wrapper {
@include mobile {
margin-top: 1.2rem;
@@ -51,6 +56,7 @@
}
}
}
+
&__drop-down {
display: inline-block;
min-width: 20.3rem;
@@ -58,9 +64,11 @@
line-height: 1.43;
text-align: left;
margin: 0 auto 5.1rem;
+
@include desktop {
max-width: 40rem;
}
+
@include mobile {
width: 100%;
min-width: auto;
@@ -71,21 +79,26 @@
@include desktop {
margin: 0 auto 3.3rem;
}
+
@include mobile {
margin-bottom: 2.4rem;
}
}
+
& .dc-dropdown__display-text {
width: 100%;
align-items: center;
}
+
& .dc-field--error {
padding-top: 0.5rem;
+
.link {
pointer-events: all;
}
}
}
+
&__notes {
@include mobile {
padding: 1.6rem 0;
@@ -99,6 +112,7 @@
margin-top: 0;
}
}
+
&__bullet {
background-color: var(--text-general);
border-radius: 100%;
@@ -113,10 +127,12 @@
margin-bottom: 0.8rem;
}
}
+
&__wrapper {
.cashier__form-submit {
margin-top: 3rem;
}
+
.account-transfer-form__input {
min-width: 40rem;
height: 6.5rem;
@@ -125,12 +141,15 @@
.dc-input__hint {
margin: 0.5rem 0 -1.9rem 1.3rem;
}
+
.dc-field--error {
margin-top: 0.5rem;
}
+
&-fit-content {
width: fit-content;
}
+
@include mobile {
width: 100%;
min-width: auto;
@@ -141,14 +160,17 @@
}
}
}
+
&__currency,
&__balance {
line-height: 1.43;
}
+
&__currency {
&-icon {
align-self: center;
}
+
&-wrapper {
display: flex;
flex-direction: column;
@@ -158,13 +180,16 @@
margin-right: 8px;
}
}
+
&__icon {
display: flex;
justify-content: center;
}
+
&__balance {
margin-left: auto;
}
+
&__form-submit {
display: flex;
flex-direction: column;
@@ -175,6 +200,7 @@
> * {
width: auto;
}
+
&--align-end {
align-items: flex-end;
@@ -187,7 +213,7 @@
&__form-buttons {
display: flex;
flex-direction: row;
- justify-content: end;
+ justify-content: flex-end;
align-items: center;
margin-top: 4rem;
@@ -213,11 +239,13 @@
&-wrapper {
display: flex;
height: 100%;
+
@include mobile {
height: initial;
}
}
}
+
@include desktop {
&__form-submit {
min-width: 36rem;
@@ -226,14 +254,17 @@
}
}
}
+
.dc-modal__container_account_transfer {
&_switch_modal {
transition: none;
+
.dc-modal {
@include mobile() {
&-header__close {
margin: 2.4rem 1.6rem 0;
}
+
&-body {
font-size: 1.4rem;
}
@@ -242,9 +273,11 @@
&-header__title {
padding: 2.4rem 2.4rem 0;
}
+
&-body {
padding: 2.4rem;
}
+
&-footer {
padding: 0 2.4rem 2.4rem;
}
diff --git a/packages/cashier/src/pages/payment-agent/payment-agent-list/payment-agent-list.scss b/packages/cashier/src/pages/payment-agent/payment-agent-list/payment-agent-list.scss
index 39618819c1bc..221249d604c6 100644
--- a/packages/cashier/src/pages/payment-agent/payment-agent-list/payment-agent-list.scss
+++ b/packages/cashier/src/pages/payment-agent/payment-agent-list/payment-agent-list.scss
@@ -7,6 +7,9 @@
padding: 0;
margin: 0;
}
+ .email-verification-empty-state {
+ margin-top: 2.4rem;
+ }
&__instructions {
@include mobile {
display: grid;
diff --git a/packages/cashier/src/pages/withdrawal/crypto-withdraw-form/crypto-withdraw-form.tsx b/packages/cashier/src/pages/withdrawal/crypto-withdraw-form/crypto-withdraw-form.tsx
index 1c0c6726b34b..cf2b1ca536f3 100644
--- a/packages/cashier/src/pages/withdrawal/crypto-withdraw-form/crypto-withdraw-form.tsx
+++ b/packages/cashier/src/pages/withdrawal/crypto-withdraw-form/crypto-withdraw-form.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { Button, Icon, Input, Loading, Text } from '@deriv/components';
import { useCurrentAccountDetails } from '@deriv/hooks';
-import { CryptoConfig, getCurrencyName, isMobile } from '@deriv/shared';
+import { CryptoConfig, getCurrencyName } from '@deriv/shared';
import { observer, useStore } from '@deriv/stores';
import { Localize, localize } from '@deriv/translations';
import classNames from 'classnames';
@@ -48,7 +48,8 @@ const Header = ({ currency }: THeaderProps) => {
};
const CryptoWithdrawForm = observer(() => {
- const { client } = useStore();
+ const { client, ui } = useStore();
+ const { is_mobile } = ui;
const {
balance,
currency,
@@ -101,11 +102,11 @@ const CryptoWithdrawForm = observer(() => {
return (
- {!isMobile() &&
}
-
-
+ {!is_mobile &&
}
+
+
- {isMobile() &&
}
+ {is_mobile &&
}
({
+ ...jest.requireActual('@deriv/api'),
+ useRequest: jest.fn(),
+}));
+
+// @ts-expect-error ignore this until find a way to make arguments as partial
+const mockUseRequest = useRequest as jest.MockedFunction>;
+
+const mock_store = mockStore({
+ client: {
+ email: 'john@company.com',
+ },
+ modules: { cashier: { transaction_history: { onMount: jest.fn() } } },
+});
+
+describe('WithdrawalVerificationEmail', () => {
+ test('should render the component', () => {
+ // @ts-expect-error ignore this until find a way to make arguments as partial
+ mockUseRequest.mockReturnValue({});
+
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(screen.queryByTestId('dt_empty_state_title')).toBeInTheDocument();
+ expect(screen.queryByTestId('dt_empty_state_description')).toBeInTheDocument();
+ expect(screen.queryByTestId('dt_empty_state_action')).toHaveTextContent('Send email');
+ });
+
+ test('should show the error component when `error` is provided', () => {
+ // @ts-expect-error ignore this until find a way to make arguments as partial
+ mockUseRequest.mockReturnValue({ error: { code: 'CODE', message: 'foo' } });
+
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(screen.getByText('foo')).toBeInTheDocument();
+ });
+
+ test('should show the proper message when email has been sent.', () => {
+ // @ts-expect-error ignore this until find a way to make arguments as partial
+ mockUseRequest.mockReturnValue({ mutate: jest.fn() });
+
+ render(, {
+ wrapper: ({ children }) => {children},
+ });
+
+ const send_button = screen.getByText('Send email');
+ fireEvent.click(send_button);
+
+ expect(screen.getByText("We've sent you an email.")).toBeInTheDocument();
+ });
+});
diff --git a/packages/cashier/src/pages/withdrawal/withdrawal-verification-email/withdrawal-verification-email.tsx b/packages/cashier/src/pages/withdrawal/withdrawal-verification-email/withdrawal-verification-email.tsx
index 4c27bcd72789..c71074f27156 100644
--- a/packages/cashier/src/pages/withdrawal/withdrawal-verification-email/withdrawal-verification-email.tsx
+++ b/packages/cashier/src/pages/withdrawal/withdrawal-verification-email/withdrawal-verification-email.tsx
@@ -16,11 +16,11 @@ const WithdrawalVerificationEmail = observer(() => {
return (
-
+
diff --git a/packages/cfd/package.json b/packages/cfd/package.json
index df4d52d2ad50..ad79b8b8496f 100644
--- a/packages/cfd/package.json
+++ b/packages/cfd/package.json
@@ -85,7 +85,7 @@
"@deriv/account": "^1.0.0",
"@deriv/api-types": "^1.0.118",
"@deriv/components": "^1.0.0",
- "@deriv/deriv-api": "^1.0.11",
+ "@deriv/deriv-api": "^1.0.13",
"@deriv/hooks": "^1.0.0",
"@deriv/shared": "^1.0.0",
"@deriv/stores": "^1.0.0",
diff --git a/packages/cfd/src/Components/cfd-account-card.tsx b/packages/cfd/src/Components/cfd-account-card.tsx
index 3687a4086600..2a277397ae86 100644
--- a/packages/cfd/src/Components/cfd-account-card.tsx
+++ b/packages/cfd/src/Components/cfd-account-card.tsx
@@ -1,29 +1,32 @@
-import classNames from 'classnames';
import React from 'react';
import { CSSTransition } from 'react-transition-group';
-import { Icon, Money, Button, Text, DesktopWrapper, MobileWrapper, Popover } from '@deriv/components';
-import { isMobile, mobileOSDetect, getCFDPlatformLabel, CFD_PLATFORMS } from '@deriv/shared';
-import { localize, Localize } from '@deriv/translations';
-import { CFDAccountCopy } from './cfd-account-copy';
+import classNames from 'classnames';
+import { FormikValues } from 'formik';
+
+import { DetailsOfEachMT5Loginid } from '@deriv/api-types';
+import { Button, DesktopWrapper, Icon, MobileWrapper, Money, Popover, Text } from '@deriv/components';
+import { CFD_PLATFORMS, getCFDPlatformLabel, isMobile, mobileOSDetect } from '@deriv/shared';
+import { observer, useStore } from '@deriv/stores';
+import { Localize, localize } from '@deriv/translations';
+
import {
- getDXTradeWebTerminalLink,
- getPlatformDXTradeDownloadLink,
getCTraderWebTerminalLink,
getDerivEzWebTerminalLink,
+ getDXTradeWebTerminalLink,
+ getPlatformDXTradeDownloadLink,
} from '../Helpers/constants';
+import { useCfdStore } from '../Stores/Modules/CFD/Helpers/useCfdStores';
+
+import { CFDAccountCopy } from './cfd-account-copy';
import {
TAccountIconValues,
- TSpecBoxProps,
- TPasswordBoxProps,
- TCFDAccountCardActionProps,
TCFDAccountCard,
+ TCFDAccountCardActionProps,
+ TPasswordBoxProps,
+ TSpecBoxProps,
TTradingPlatformAccounts,
TTradingPlatformAvailableAccount,
} from './props.types';
-import { DetailsOfEachMT5Loginid } from '@deriv/api-types';
-import { useStore, observer } from '@deriv/stores';
-import { useCfdStore } from '../Stores/Modules/CFD/Helpers/useCfdStores';
-import { FormikValues } from 'formik';
const account_icons: { [key: string]: TAccountIconValues } = {
mt5: {
diff --git a/packages/cfd/src/Components/specbox.tsx b/packages/cfd/src/Components/specbox.tsx
index a7ffdff76385..7af8302c1ff8 100644
--- a/packages/cfd/src/Components/specbox.tsx
+++ b/packages/cfd/src/Components/specbox.tsx
@@ -1,6 +1,8 @@
import React from 'react';
import classNames from 'classnames';
+
import { Text } from '@deriv/components';
+
import { CFDAccountCopy } from './cfd-account-copy';
export type TSpecBoxProps = {
diff --git a/packages/cfd/src/Containers/compare-accounts-content.tsx b/packages/cfd/src/Containers/compare-accounts-content.tsx
index f0c08058f2b5..5df647c940ef 100644
--- a/packages/cfd/src/Containers/compare-accounts-content.tsx
+++ b/packages/cfd/src/Containers/compare-accounts-content.tsx
@@ -1,8 +1,9 @@
import React from 'react';
-import { Table, Text, ThemedScrollbars, Div100vhContainer } from '@deriv/components';
-import { localize, Localize } from '@deriv/translations';
-import { isDesktop, CFD_PLATFORMS, isLandingCompanyEnabled } from '@deriv/shared';
+
import { LandingCompany } from '@deriv/api-types';
+import { Div100vhContainer, Table, Text, ThemedScrollbars } from '@deriv/components';
+import { CFD_PLATFORMS, isDesktop, isLandingCompanyEnabled } from '@deriv/shared';
+import { Localize, localize } from '@deriv/translations';
type TCFDAttributeDescriberProps = {
name: string;
diff --git a/packages/cfd/src/Containers/dmt5-trade-modal.tsx b/packages/cfd/src/Containers/dmt5-trade-modal.tsx
index 3ede695524ec..af33377a23e8 100644
--- a/packages/cfd/src/Containers/dmt5-trade-modal.tsx
+++ b/packages/cfd/src/Containers/dmt5-trade-modal.tsx
@@ -1,21 +1,24 @@
import React from 'react';
-import { Text, Icon, Money } from '@deriv/components';
-import { TTradingPlatformAccounts } from '../Components/props.types';
+
import { DetailsOfEachMT5Loginid } from '@deriv/api-types';
+import { Icon, Money, Text } from '@deriv/components';
import {
CFD_PLATFORMS,
- isMobile,
getCFDAccountDisplay,
+ getCFDAccountKey,
getCFDPlatformLabel,
getPlatformSettings,
getUrlBase,
- getCFDAccountKey,
+ isMobile,
} from '@deriv/shared';
import { Localize, localize } from '@deriv/translations';
-import SpecBox from '../Components/specbox';
-import PasswordBox from '../Components/passwordbox';
-import { getPlatformMt5DownloadLink, getMT5WebTerminalLink } from '../Helpers/constants';
+
import TradingPlatformIcon from '../Assets/svgs/trading-platform';
+import PasswordBox from '../Components/passwordbox';
+import { TTradingPlatformAccounts } from '../Components/props.types';
+import SpecBox from '../Components/specbox';
+import { getMT5WebTerminalLink, getPlatformMt5DownloadLink } from '../Helpers/constants';
+
import { TCFDPasswordReset } from './props.types';
type TMT5TradeModalProps = {
diff --git a/packages/cfd/src/Stores/Modules/CFD/cfd-store.js b/packages/cfd/src/Stores/Modules/CFD/cfd-store.js
index 2a00f2728dc5..20723e8fda12 100644
--- a/packages/cfd/src/Stores/Modules/CFD/cfd-store.js
+++ b/packages/cfd/src/Stores/Modules/CFD/cfd-store.js
@@ -1,6 +1,6 @@
import { action, computed, observable, reaction, runInAction, makeObservable, override } from 'mobx';
import { getAccountListKey, getAccountTypeFields, CFD_PLATFORMS, WS, Jurisdiction } from '@deriv/shared';
-import BaseStore from 'Stores/base-store';
+import BaseStore from '../../base-store';
import { getDxCompanies, getMtCompanies, getDerivezCompanies } from './Helpers/cfd-config';
export default class CFDStore extends BaseStore {
diff --git a/packages/cfd/src/Stores/base-store.js b/packages/cfd/src/Stores/base-store.js
index 4e005596209a..9dd1131f5da7 100644
--- a/packages/cfd/src/Stores/base-store.js
+++ b/packages/cfd/src/Stores/base-store.js
@@ -1,5 +1,6 @@
-import { action, intercept, observable, reaction, toJS, when, makeObservable } from 'mobx';
-import { isProduction, isEmptyObject, Validator } from '@deriv/shared';
+import { action, intercept, makeObservable,observable, reaction, toJS, when } from 'mobx';
+
+import { isEmptyObject, isProduction, Validator } from '@deriv/shared';
/**
* BaseStore class is the base class for all defined stores in the application. It handles some stuff such as:
diff --git a/packages/components/src/components/amount-input/__tests__/amount-input.spec.tsx b/packages/components/src/components/amount-input/__tests__/amount-input.spec.tsx
new file mode 100644
index 000000000000..29d8b6a8d27c
--- /dev/null
+++ b/packages/components/src/components/amount-input/__tests__/amount-input.spec.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import AmountInput from '../amount-input';
+
+describe('', () => {
+ it('should render with the initial value of "0.00" if {initial_value} is not specified', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ expect(input).toHaveDisplayValue('0.00');
+ });
+
+ it('should render with the correct initial value if {initial_value} was supplied', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ expect(input).toHaveDisplayValue('42.00');
+ });
+
+ it('should not change the value on non-numeric and non-"." inputs', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ userEvent.type(input, 'abcdef!@#$%^&*()_+-={}[];\'"|\\/,.<>');
+ expect(input).toHaveDisplayValue('0.00');
+ });
+
+ it('should change the value like an ATM, i.e. from right to left, when entering digits', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ userEvent.type(input, '1');
+ expect(input).toHaveDisplayValue('0.01');
+ userEvent.type(input, '2');
+ expect(input).toHaveDisplayValue('0.12');
+ userEvent.type(input, '3');
+ expect(input).toHaveDisplayValue('1.23');
+ });
+
+ it('should add commas for big values', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ userEvent.type(input, '123456789');
+ expect(input).toHaveDisplayValue('1,234,567.89');
+ });
+
+ it('should not remove "0.00" when backspacing', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ userEvent.type(input, '100');
+ expect(input).toHaveDisplayValue('1.00');
+ userEvent.clear(input);
+ expect(input).toHaveDisplayValue('0.00');
+ });
+
+ it('should not accept more than {maxDigits} digits', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ userEvent.type(input, '1234567890987654321');
+ expect(input).toHaveDisplayValue('1,234,567.89');
+ });
+
+ it('should work correctly with explicitly set {decimal_points}', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ expect(input).toHaveDisplayValue('0.00000');
+ userEvent.type(input, '12345678');
+ expect(input).toHaveDisplayValue('123.45678');
+ });
+
+ it('should allow pasting numbers and then interpret those correctly', () => {
+ render();
+ const input = screen.getByTestId('dt_amount-input');
+ const values = [
+ ['123', '123.00'],
+ ['123.42', '123.42'],
+ ['123,42', '123.42'],
+ ['123,000.00', '123,000.00'],
+ ];
+ values.forEach(pair => {
+ userEvent.clear(input);
+ userEvent.click(input);
+ userEvent.paste(input, pair[0]);
+ expect(input).toHaveDisplayValue(pair[1]);
+ });
+ });
+});
diff --git a/packages/components/src/components/amount-input/amount-input.scss b/packages/components/src/components/amount-input/amount-input.scss
new file mode 100644
index 000000000000..0a4b9f2f84a8
--- /dev/null
+++ b/packages/components/src/components/amount-input/amount-input.scss
@@ -0,0 +1,51 @@
+.amount-input-wrapper {
+ display: flex;
+ flex-direction: column;
+ padding: 0.8rem;
+ height: auto;
+}
+
+.amount-input-container {
+ position: relative;
+ height: 2.8rem;
+
+ .dc-input__field {
+ color: var(--text-general);
+ }
+
+ &--error {
+ .dc-input__field {
+ color: var(--text-loss-danger);
+ -webkit-text-fill-color: var(--text-loss-danger);
+ }
+ }
+
+ &--disabled {
+ .dc-input__field {
+ color: var(--text-disabled-1);
+ }
+ }
+
+ @include mobile {
+ height: 2.4rem;
+ }
+}
+
+.amount-input {
+ position: absolute;
+ border: none;
+ flex: 1;
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+
+ input {
+ font-size: var(--text-size-sm);
+ font-weight: var(--text-weight-bold);
+
+ @include mobile {
+ font-size: var(--text-size-s);
+ }
+ }
+}
diff --git a/packages/components/src/components/amount-input/amount-input.tsx b/packages/components/src/components/amount-input/amount-input.tsx
new file mode 100644
index 000000000000..95f1b21814e3
--- /dev/null
+++ b/packages/components/src/components/amount-input/amount-input.tsx
@@ -0,0 +1,151 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import classNames from 'classnames';
+import { isMobile } from '@deriv/shared';
+import Input from '../input';
+import Text from '../text';
+
+type TAmountInput = {
+ currency: string;
+ decimal_places?: number;
+ disabled?: boolean;
+ has_error?: boolean;
+ initial_value?: number;
+ label?: string;
+ locale?: Intl.LocalesArgument;
+ max_digits?: number;
+ onChange?: (value: number) => void;
+};
+
+const AmountInput = ({
+ currency,
+ decimal_places = 2,
+ disabled = false,
+ has_error,
+ initial_value = 0,
+ label,
+ locale,
+ max_digits = 8,
+ onChange,
+}: TAmountInput) => {
+ const [value, setValue] = useState(initial_value);
+ const [focus, setFocus] = useState(false);
+ const [is_pasting, setIsPasting] = useState(false);
+ const [caret_right_offset, setCaretRightOffset] = useState(0);
+ const [selection, setSelection] = useState<{
+ selectionStart: number;
+ selectionEnd: number;
+ }>({ selectionStart: 0, selectionEnd: 0 });
+ const [target, setTarget] = useState['target']>();
+
+ const displayNumber = useCallback(
+ (number: number) => number.toLocaleString(locale, { minimumFractionDigits: decimal_places }),
+ [decimal_places, locale]
+ );
+
+ useEffect(() => {
+ setValue(initial_value);
+ }, [initial_value]);
+
+ useEffect(() => {
+ // update caret position every time the value changes (this happens after onChange)
+ const updated_caret_position = displayNumber(value).length - caret_right_offset;
+ target?.setSelectionRange(updated_caret_position, updated_caret_position);
+ setSelection({ selectionStart: updated_caret_position, selectionEnd: updated_caret_position });
+ }, [value, target, displayNumber]);
+
+ const onChangeHandler: React.ComponentProps['onChange'] = e => {
+ if (!target) setTarget(e.target);
+ let new_value = value;
+ if (!is_pasting) {
+ // handle ATM typing:
+ // remove all characters that are not digit / point / comma:
+ const input_value = e.target.value.replace(/[^\d.,]/g, '');
+ if (input_value.replace(/[.,]/g, '').replace(/^0+/g, '').length <= max_digits)
+ new_value = Number(input_value.replace(/[.,]/g, '')) / Math.pow(10, decimal_places);
+ } else {
+ // handle pasting:
+ const selection_length = selection.selectionEnd - selection.selectionStart;
+ const pasted_string_length = e.target.value.length + selection_length - displayNumber(value).length;
+ const pasted_string = e.target.value.substring(
+ selection.selectionStart,
+ selection.selectionStart + pasted_string_length
+ );
+ // remove all characters that are not digit / point / comma:
+ const input_value = e.target.value.replace(/[^\d.,]/g, '');
+ // understand the value user wants to paste:
+ const pasted_value = pasted_string
+ .replace(/[^\d.,]/g, '') // remove all characters that are not digit / point / comma
+ .replace(/[,.](?=.*[,.])/g, '') // leave only the last point / comma
+ .replace(',', '.'); // make the last point / comma separator a point
+ if (pasted_value.replace('.', '')) {
+ // if the value is a valid non-empty string, handle the two scenarios:
+ if ((value === 0 && caret_right_offset === 0) || selection_length === displayNumber(value).length) {
+ // handle pasting when there's nothing entered before it, or it is overridden (intention: reset value):
+ new_value = Number(
+ pasted_value.substring(
+ 0,
+ pasted_value.includes('.') ? max_digits + 1 : max_digits - decimal_places
+ )
+ );
+ } else if (input_value.replace(/[.,]/g, '').replace(/^0+/g, '').length <= max_digits) {
+ // handle pasting when there's something entered before it and there's space for the pasted value (intention: add to value):
+ new_value = Number(input_value.replace(/[.,]/g, '')) / Math.pow(10, decimal_places);
+ }
+ }
+ }
+ setValue(new_value);
+ setIsPasting(false);
+ onChange?.(new_value);
+ };
+
+ const inputActionHandler: React.ComponentProps['onMouseDown'] &
+ React.ComponentProps['onMouseUp'] &
+ React.ComponentProps['onKeyDown'] = e => {
+ if (e.currentTarget.selectionStart !== null && e.currentTarget.selectionEnd !== null) {
+ setCaretRightOffset(e.currentTarget.value.length - e.currentTarget.selectionEnd);
+ setSelection({
+ selectionStart: e.currentTarget.selectionStart,
+ selectionEnd: e.currentTarget.selectionEnd,
+ });
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default AmountInput;
diff --git a/packages/components/src/components/amount-input/index.ts b/packages/components/src/components/amount-input/index.ts
new file mode 100644
index 000000000000..ee447d524403
--- /dev/null
+++ b/packages/components/src/components/amount-input/index.ts
@@ -0,0 +1,4 @@
+import AmountInput from './amount-input';
+import './amount-input.scss';
+
+export default AmountInput;
diff --git a/packages/components/src/components/app-linked-with-wallet-icon/app-linked-with-wallet-icon.scss b/packages/components/src/components/app-linked-with-wallet-icon/app-linked-with-wallet-icon.scss
new file mode 100644
index 000000000000..afc5625a4566
--- /dev/null
+++ b/packages/components/src/components/app-linked-with-wallet-icon/app-linked-with-wallet-icon.scss
@@ -0,0 +1,56 @@
+@mixin container-space($parent, $space) {
+ #{$parent}__top-icon {
+ top: $space * 1rem;
+ left: $space * 1rem;
+ }
+
+ #{$parent}__bottom-icon {
+ bottom: $space * 1rem;
+ right: $space * 1rem;
+ }
+}
+
+.app-icon {
+ $p-app-icon: &;
+
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: $BORDER_RADIUS;
+ overflow: hidden;
+
+ &__top-icon {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ }
+
+ &__bottom-icon {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ }
+
+ &--small {
+ width: 4rem;
+ height: 2.4rem;
+
+ @include container-space($p-app-icon, 0.1);
+ }
+
+ &--medium {
+ width: 6.4rem;
+ height: 4rem;
+
+ @include container-space($p-app-icon, 0.2);
+ }
+
+ &--large {
+ width: 12.8rem;
+ height: 8rem;
+
+ @include container-space($p-app-icon, 0.4);
+ }
+}
diff --git a/packages/components/src/components/app-linked-with-wallet-icon/app-linked-with-wallet-icon.tsx b/packages/components/src/components/app-linked-with-wallet-icon/app-linked-with-wallet-icon.tsx
new file mode 100644
index 000000000000..4b8942096d31
--- /dev/null
+++ b/packages/components/src/components/app-linked-with-wallet-icon/app-linked-with-wallet-icon.tsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { WalletIcon } from '../wallet-icon';
+import './app-linked-with-wallet-icon.scss';
+
+type TAppIconProps = {
+ app_icon: string;
+ gradient_class: string;
+ has_bg?: boolean;
+ hide_watermark?: boolean;
+ size?: 'small' | 'medium' | 'large';
+ type: React.ComponentProps['type'];
+ wallet_icon: string;
+};
+
+/**
+ * Use the WalletIcon sizes
+ */
+const sizes = {
+ top: {
+ small: 'small',
+ medium: 'medium',
+ large: 'xlarge',
+ },
+ bottom: {
+ small: 'xsmall',
+ medium: 'small',
+ large: 'large',
+ },
+} as const;
+
+const AppLinkedWithWalletIcon = ({
+ app_icon,
+ gradient_class,
+ hide_watermark,
+ size = 'medium',
+ type,
+ wallet_icon,
+}: TAppIconProps) => {
+ if (!app_icon || !wallet_icon || !gradient_class) {
+ return null;
+ }
+
+ return (
+
+ {/* Top Icon */}
+
+
+
+
+ {/* Bottom Icon */}
+
+
+
+
+ );
+};
+
+export default AppLinkedWithWalletIcon;
diff --git a/packages/components/src/components/app-linked-with-wallet-icon/index.ts b/packages/components/src/components/app-linked-with-wallet-icon/index.ts
new file mode 100644
index 000000000000..b601135cad4c
--- /dev/null
+++ b/packages/components/src/components/app-linked-with-wallet-icon/index.ts
@@ -0,0 +1,3 @@
+import AppLinkedWithWalletIcon from './app-linked-with-wallet-icon';
+
+export { AppLinkedWithWalletIcon };
diff --git a/packages/components/src/components/badge/__tests__/badge.spec.tsx b/packages/components/src/components/badge/__tests__/badge.spec.tsx
new file mode 100644
index 000000000000..5d93634423ad
--- /dev/null
+++ b/packages/components/src/components/badge/__tests__/badge.spec.tsx
@@ -0,0 +1,107 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { isMobile } from '@deriv/shared';
+import Badge from '../badge';
+
+jest.mock('@deriv/shared', () => ({
+ ...jest.requireActual('@deriv/shared'),
+ isMobile: jest.fn(),
+}));
+
+describe('Badge component', () => {
+ beforeEach(() => {
+ (isMobile as jest.Mock).mockReturnValue(true);
+ });
+
+ afterEach(() => {
+ (isMobile as jest.Mock).mockReset();
+ });
+
+ it('Should render properly with default values and label', () => {
+ render();
+ expect(screen.getByText('Badge')).toBeInTheDocument();
+ });
+
+ it('Should render proper medium badge in mobile view', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('height: 1.2rem');
+ expect(badge).toHaveStyle('padding-inline: 0.4rem');
+ });
+
+ it('Should render proper medium badge in desktop view', () => {
+ (isMobile as jest.Mock).mockReturnValue(false);
+
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('height: 1.4rem');
+ expect(badge).toHaveStyle('padding-inline: 0.4rem');
+ });
+
+ it('Should render proper large badge in mobile view', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('height: 1.6rem');
+ expect(badge).toHaveStyle('padding-inline: 0.8rem');
+ });
+
+ it('Should render proper large badge in desktop view', () => {
+ (isMobile as jest.Mock).mockReturnValue(false);
+
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('height: 2.2rem');
+ expect(badge).toHaveStyle('padding-inline: 0.8rem');
+ });
+
+ it('Should render badge with normal font weight', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('--text-weight: var(--text-weight-normal)');
+ });
+
+ it('Should render badge with normal font weight', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('--text-weight: var(--text-weight-bold)');
+ });
+
+ it('Should render badge with proper font-size in mobile view', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('--text-size: var(--text-size-xxxxs)');
+ });
+
+ it('Should render badge with proper font-size in desktop view', () => {
+ (isMobile as jest.Mock).mockReturnValue(false);
+
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveStyle('--text-size: var(--text-size-xxxs)');
+ });
+
+ it('Should render badge with proper amount of rounded corners', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveClass('dc-badge--two-rounded');
+ });
+
+ it('Should render badge with bordered type', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveClass('dc-badge--bordered');
+ });
+
+ it('Should render badge with contained type', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveClass('dc-badge--contained');
+ });
+
+ it('Should render badge with contained type and red background', () => {
+ render();
+ const badge = screen.getByText('Badge');
+ expect(badge).toHaveClass('dc-badge--contained dc-badge--red');
+ expect(badge).toHaveClass('dc-badge--contained dc-badge--red');
+ });
+});
diff --git a/packages/components/src/components/badge/badge.scss b/packages/components/src/components/badge/badge.scss
new file mode 100644
index 000000000000..70392d4057ba
--- /dev/null
+++ b/packages/components/src/components/badge/badge.scss
@@ -0,0 +1,40 @@
+.dc-badge {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ color: $color-white;
+
+ &--contained {
+ border: none;
+ }
+
+ &--bordered {
+ border: 1px solid var(--text-prominent);
+ color: var(--text-prominent);
+ }
+
+ &--blue {
+ background-color: $color-blue;
+ }
+
+ &--orange {
+ background-color: $color-orange;
+ }
+
+ &--red {
+ background-color: $color-red;
+ }
+
+ &--gray {
+ background-color: $color-grey-5;
+ }
+
+ &--full-rounded {
+ border-radius: $BORDER_RADIUS/2;
+ }
+
+ &--two-rounded {
+ border-bottom-left-radius: $BORDER_RADIUS * 2;
+ border-top-right-radius: $BORDER_RADIUS * 2;
+ }
+}
diff --git a/packages/components/src/components/badge/badge.tsx b/packages/components/src/components/badge/badge.tsx
new file mode 100644
index 000000000000..b588b1a3fd47
--- /dev/null
+++ b/packages/components/src/components/badge/badge.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import classNames from 'classnames';
+import { isMobile } from '@deriv/shared';
+import Text from '../text';
+
+type TBadgeSize = 'medium' | 'large';
+type TWeight = 'bold' | 'normal';
+type TBackgroundColor = 'blue' | 'orange' | 'red' | 'gray';
+type TRoundedCorners = 4 | 2;
+
+interface Badge extends React.HTMLAttributes {
+ className?: string;
+ custom_color?: string;
+ label: string;
+ rounded_corners?: TRoundedCorners;
+ size?: TBadgeSize;
+ weight?: TWeight;
+}
+
+interface BadgeContained extends Badge {
+ type: 'contained';
+ background_color: TBackgroundColor;
+}
+
+interface BadgeBordered extends Badge {
+ type: 'bordered';
+}
+
+type BadgeProps = BadgeContained | BadgeBordered;
+
+const Badge = (props: BadgeProps) => {
+ const {
+ className,
+ custom_color,
+ label,
+ rounded_corners = 4,
+ size = 'medium',
+ type,
+ weight = 'bold',
+ ...rest
+ } = props;
+
+ const is_contained = type === 'contained';
+ const is_bordered = type === 'bordered';
+
+ const badge_height = React.useMemo(() => {
+ switch (size) {
+ case 'large':
+ return {
+ height: isMobile() ? '1.6rem' : '2.2rem',
+ paddingInline: '0.8rem',
+ };
+ case 'medium':
+ default:
+ return {
+ height: isMobile() ? '1.2rem' : '1.4rem',
+ paddingInline: '0.4rem',
+ };
+ }
+ }, [size]);
+
+ const badge_class_names = React.useMemo(() => {
+ return classNames(
+ 'dc-badge',
+ {
+ 'dc-badge--contained': is_contained,
+ 'dc-badge--bordered': is_bordered,
+ 'dc-badge--blue': is_contained && props.background_color === 'blue',
+ 'dc-badge--orange': is_contained && props.background_color === 'orange',
+ 'dc-badge--red': is_contained && props.background_color === 'red',
+ 'dc-badge--gray': is_contained && props.background_color === 'gray',
+ 'dc-badge--full-rounded': rounded_corners === 4,
+ 'dc-badge--two-rounded': rounded_corners === 2,
+ },
+ className
+ );
+ }, [className, is_bordered, is_contained, is_contained && props.background_color, rounded_corners]);
+
+ if (!label) return null;
+
+ return (
+
+ {label}
+
+ );
+};
+
+export default Badge;
diff --git a/packages/components/src/components/badge/index.ts b/packages/components/src/components/badge/index.ts
new file mode 100644
index 000000000000..df4fc62f3ce8
--- /dev/null
+++ b/packages/components/src/components/badge/index.ts
@@ -0,0 +1,4 @@
+import Badge from './badge';
+import './badge.scss';
+
+export default Badge;
diff --git a/packages/components/src/components/contract-card/contract-card-items/toggle-card-dialog.tsx b/packages/components/src/components/contract-card/contract-card-items/toggle-card-dialog.tsx
index ce87b3690213..1ef02a0e921f 100644
--- a/packages/components/src/components/contract-card/contract-card-items/toggle-card-dialog.tsx
+++ b/packages/components/src/components/contract-card/contract-card-items/toggle-card-dialog.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { isDesktop, isMobile } from '@deriv/shared';
import ContractCardDialog from './contract-card-dialog';
import ContractUpdateForm from './contract-update-form';
+import PopoverMessageCheckbox from '../../popover-message-checkbox';
import Icon from '../../icon';
import DesktopWrapper from '../../desktop-wrapper';
import MobileDialog from '../../mobile-dialog';
@@ -56,7 +57,7 @@ const ToggleCardDialog = ({
const [is_visible, setIsVisible] = React.useState(false);
const [top, setTop] = React.useState(0);
const [left, setLeft] = React.useState(0);
- const is_do_not_show_selected = !should_show_cancellation_warning;
+ const [should_hide_selected, setShouldHideSelected] = React.useState(!should_show_cancellation_warning);
const toggle_ref = React.useRef(null);
const dialog_ref = React.useRef(null);
@@ -93,11 +94,15 @@ const ToggleCardDialog = ({
};
const onPopoverClose = () => {
- if (is_do_not_show_selected) {
+ if (should_hide_selected) {
toggleCancellationWarning?.();
}
};
+ const onPopoverCheckboxChange = () => {
+ setShouldHideSelected(!should_hide_selected);
+ };
+
const toggleDialog = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
@@ -133,7 +138,15 @@ const ToggleCardDialog = ({
is_bubble_hover_enabled
margin={2}
zIndex='2'
- message={getCardLabels().TAKE_PROFIT_LOSS_NOT_AVAILABLE}
+ message={
+
+ }
onBubbleClose={onPopoverClose}
>
{edit_icon}
diff --git a/packages/components/src/components/date-picker/index.ts b/packages/components/src/components/date-picker/index.ts
index adfc2a303a54..e1fd3c52ef90 100644
--- a/packages/components/src/components/date-picker/index.ts
+++ b/packages/components/src/components/date-picker/index.ts
@@ -1,3 +1,4 @@
import DatePicker from './date-picker';
import './date-picker.scss';
+
export default DatePicker;
diff --git a/packages/components/src/components/div100vh-container/div100vh-container.tsx b/packages/components/src/components/div100vh-container/div100vh-container.tsx
index 619466ffa4ca..426772e5a777 100644
--- a/packages/components/src/components/div100vh-container/div100vh-container.tsx
+++ b/packages/components/src/components/div100vh-container/div100vh-container.tsx
@@ -15,14 +15,14 @@ import Div100vh from 'react-div-100vh';
/* To bypass usage of component altogether, use is_bypassed */
type TDiv100vhContainer = {
- id?: string;
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,
diff --git a/packages/components/src/components/empty-state/empty-state.scss b/packages/components/src/components/empty-state/empty-state.scss
index c8d32e5fc7d1..36dc95f1aede 100644
--- a/packages/components/src/components/empty-state/empty-state.scss
+++ b/packages/components/src/components/empty-state/empty-state.scss
@@ -1,8 +1,19 @@
.empty-state {
width: 100%;
display: flex;
- flex: 1;
flex-direction: column;
align-items: center;
- gap: 1.4rem;
+ gap: 2.4rem;
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: 0.8rem;
+ }
+
+ &__action .dc-btn__text {
+ @include mobile {
+ font-size: var(--text-size-xxs);
+ }
+ }
}
diff --git a/packages/components/src/components/empty-state/empty-state.tsx b/packages/components/src/components/empty-state/empty-state.tsx
index c56b8a6ccc3e..21f9cf1bbca6 100644
--- a/packages/components/src/components/empty-state/empty-state.tsx
+++ b/packages/components/src/components/empty-state/empty-state.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { isMobile } from '@deriv/shared';
import Button from '../button';
import Icon from '../icon';
import Text from '../text';
@@ -20,18 +21,18 @@ export type TProps = {
const EmptyState: React.FC = ({ icon, title, description, action }) => (
{icon &&
}
-
- {title && (
-
- {title}
-
- )}
- {description && (
-
- {description}
-
- )}
-
+
+ {title && (
+
+ {title}
+
+ )}
+ {description && (
+
+ {description}
+
+ )}
+
{action && (