From 5273ee01df4d8940456591cd1cf860118992f3cd Mon Sep 17 00:00:00 2001
From: Maryia <103177211+maryia-deriv@users.noreply.github.com>
Date: Mon, 1 Apr 2024 11:53:46 +0300
Subject: [PATCH] =?UTF-8?q?Revert=20"[TRAH]=20Sergei=20/=20TRAH=20-=203006?=
=?UTF-8?q?=20/=20Trader's=20hub=20(appstore=20package)=20perform=E2=80=A6?=
=?UTF-8?q?"=20(#14413)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit c519895145706b443c8a4d3ce71b4d035ea7f2d4.
---
packages/appstore/package.json | 15 +-
.../__test__/add-more-wallets.spec.tsx | 136 +++++++
.../__test__/wallet-add-card.spec.tsx | 118 ++++++
.../add-more-wallets/add-more-wallets.scss | 91 +++++
.../add-more-wallets/add-more-wallets.tsx | 33 ++
.../add-more-wallets/carousel-buttons.tsx | 20 ++
.../add-more-wallets/carousel-container.tsx | 56 +++
.../src/components/add-more-wallets/index.ts | 3 +
.../add-more-wallets/wallet-add-card.tsx | 52 +++
.../containers/__tests__/wallets.spec.tsx | 74 ++++
.../containers/listing-container.tsx | 4 +-
.../src/components/containers/wallet.scss | 44 +++
.../src/components/containers/wallet.tsx | 39 ++
.../__tests__/demo-reset-balance.spec.tsx | 135 +++++++
.../demo-reset-balance.scss | 20 ++
.../demo-reset-balance/demo-reset-balance.tsx | 82 +++++
.../components/demo-reset-balance/index.ts | 3 +
.../__tests__/empty-state.spec.tsx | 41 +++
.../components/empty-state/empty-state.scss | 14 +
.../components/empty-state/empty-state.tsx | 26 ++
.../src/components/empty-state/index.ts | 4 +
.../__tests__/eu-disclaimer.spec.tsx | 63 ++++
.../eu-disclaimer/eu-disclaimer.scss | 24 ++
.../eu-disclaimer/eu-disclaimer.tsx | 52 +++
.../src/components/eu-disclaimer/index.ts | 3 +
.../get-more-wallet-card.scss | 24 ++
.../get-more-wallet-card.tsx | 20 ++
.../components/get-more-wallet-card/index.ts | 4 +
.../main-title-bar/__tests__/index.spec.tsx | 54 ++-
.../main-title-bar/asset-summary.tsx | 17 +-
.../account-type-modal/account-type-modal.tsx | 2 +-
.../src/components/modals/modal-manager.tsx | 6 +-
.../__tests__/real-wallets-upgrade.spec.tsx | 5 +-
.../__tests__/wallet-modal-body.spec.tsx | 119 +++++++
.../__tests__/wallet-modal-header.spec.tsx | 79 ++++
.../__tests__/wallet-modal.spec.tsx | 66 ++++
.../components/modals/wallet-modal/index.ts | 4 +
.../modals/wallet-modal/provider.tsx | 101 ++++++
.../modals/wallet-modal/wallet-modal-body.tsx | 103 ++++++
.../wallet-modal/wallet-modal-header.tsx | 102 ++++++
.../modals/wallet-modal/wallet-modal.scss | 218 ++++++++++++
.../modals/wallet-modal/wallet-modal.tsx | 80 +++++
.../appstore/src/components/routes/routes.tsx | 31 +-
.../components/toggle-account-type/index.ts | 4 +
.../stories/toggle-account-type.stories.tsx | 22 ++
.../toggle-account-type.tsx | 32 ++
.../src/components/transaction-list/index.ts | 3 +
.../non-pending-transaction.tsx | 116 ++++++
.../transaction-list/transaction-for-day.tsx | 45 +++
.../transaction-list/transaction-list.scss | 99 ++++++
.../transaction-list/transaction-list.tsx | 77 ++++
.../__tests__/wallet-button.spec.tsx | 62 ++++
.../src/components/wallet-button/index.ts | 3 +
.../wallet-button/wallet-button.scss | 79 ++++
.../wallet-button/wallet-button.tsx | 56 +++
.../__tests__/wallet-cards-carousel.spec.tsx | 165 +++++++++
.../cards-slider-swiper.tsx | 91 +++++
.../components/wallet-cards-carousel/index.ts | 3 +
.../wallet-cards-carousel.scss | 36 ++
.../wallet-cards-carousel.tsx | 35 ++
.../__tests__/wallet-content-divider.spec.tsx | 20 ++
.../__tests__/wallet-content.spec.tsx | 82 +++++
.../__tests__/wallet-transfer-block.spec.tsx | 56 +++
.../src/components/wallet-content/index.ts | 3 +
.../wallet-content/wallet-cfds-listing.tsx | 253 +++++++++++++
.../wallet-content/wallet-content-divider.tsx | 12 +
.../wallet-content/wallet-content.scss | 75 ++++
.../wallet-content/wallet-content.tsx | 35 ++
.../wallet-option-multipliers-listing.tsx | 131 +++++++
.../wallet-content/wallet-transfer-block.tsx | 50 +++
.../src/components/wallet-deposit/index.ts | 3 +
.../wallet-deposit/wallet-deposit.scss | 7 +
.../wallet-deposit/wallet-deposit.tsx | 29 ++
.../__tests__/wallet-header.spec.tsx | 336 ++++++++++++++++++
.../src/components/wallet-header/index.ts | 3 +
.../wallet-header/wallet-currency-card.tsx | 25 ++
.../wallet-header/wallet-header-balance.tsx | 67 ++++
.../wallet-header/wallet-header-buttons.tsx | 40 +++
.../wallet-header/wallet-header-title.tsx | 29 ++
.../wallet-header/wallet-header.scss | 91 +++++
.../wallet-header/wallet-header.tsx | 72 ++++
.../wallet-jurisdiction-badge.spec.tsx | 23 ++
.../wallet-jurisdiction-badge/index.ts | 3 +
.../wallet-jurisdiction-badge.tsx | 18 +
.../__tests__/wallet-transfer.spec.tsx | 47 +++
.../src/components/wallet-transfer/index.ts | 3 +
.../__tests__/transfer-account-list.spec.tsx | 98 +++++
.../transfer-account-selector.spec.tsx | 88 +++++
.../transfer-account-selector/index.ts | 3 +
.../transfer-account-list.tsx | 102 ++++++
.../transfer-account-selector.scss | 173 +++++++++
.../transfer-account-selector.tsx | 160 +++++++++
.../__tests__/wallet-transfer-tile.spec.tsx | 88 +++++
.../wallet-transfer-tile/index.ts | 3 +
.../wallet-transfer-tile.scss | 48 +++
.../wallet-transfer-tile.tsx | 123 +++++++
.../wallet-transfer/wallet-transfer.scss | 52 +++
.../wallet-transfer/wallet-transfer.tsx | 280 +++++++++++++++
.../__tests__/wallet-withdrawal.test.tsx | 36 ++
.../src/components/wallet-withdrawal/index.ts | 3 +
.../wallet-withdrawal/wallet-withdrawal.tsx | 8 +
.../appstore/src/constants/wallet-labels.ts | 25 ++
.../src/constants/wallet-mocked-response.ts | 43 +++
.../constants/wallet_description_mapper.ts | 27 ++
packages/appstore/src/declarations.d.ts | 1 +
packages/appstore/src/helpers/index.js | 1 +
packages/appstore/src/helpers/index.ts | 2 -
.../src/helpers/total-assets-helper.ts | 34 --
.../appstore/src/modules/Page404/index.tsx | 21 ++
.../traders-hub/__tests__/index.spec.tsx | 5 +-
.../modules/wallets/desktop-wallets-list.tsx | 18 +
.../appstore/src/modules/wallets/index.ts | 1 +
.../wallets/mobile-wallets-carousel.tsx | 56 +++
.../appstore/src/modules/wallets/wallets.scss | 25 ++
.../appstore/src/modules/wallets/wallets.tsx | 37 ++
packages/core/src/index.tsx | 6 +
.../src/__tests__/useExchangeRate.spec.tsx | 23 +-
packages/hooks/src/index.ts | 1 -
packages/hooks/src/useTotalAccountBalance.ts | 8 +-
packages/shared/package.json | 1 -
packages/stores/package.json | 4 +-
.../src/providers/ExchangeRatesProvider.tsx | 166 ++-------
.../src/stores/ExchangeRatesContext.tsx | 8 +-
123 files changed, 6185 insertions(+), 221 deletions(-)
create mode 100644 packages/appstore/src/components/add-more-wallets/__test__/add-more-wallets.spec.tsx
create mode 100644 packages/appstore/src/components/add-more-wallets/__test__/wallet-add-card.spec.tsx
create mode 100644 packages/appstore/src/components/add-more-wallets/add-more-wallets.scss
create mode 100644 packages/appstore/src/components/add-more-wallets/add-more-wallets.tsx
create mode 100644 packages/appstore/src/components/add-more-wallets/carousel-buttons.tsx
create mode 100644 packages/appstore/src/components/add-more-wallets/carousel-container.tsx
create mode 100644 packages/appstore/src/components/add-more-wallets/index.ts
create mode 100644 packages/appstore/src/components/add-more-wallets/wallet-add-card.tsx
create mode 100644 packages/appstore/src/components/containers/__tests__/wallets.spec.tsx
create mode 100644 packages/appstore/src/components/containers/wallet.scss
create mode 100644 packages/appstore/src/components/containers/wallet.tsx
create mode 100644 packages/appstore/src/components/demo-reset-balance/__tests__/demo-reset-balance.spec.tsx
create mode 100644 packages/appstore/src/components/demo-reset-balance/demo-reset-balance.scss
create mode 100644 packages/appstore/src/components/demo-reset-balance/demo-reset-balance.tsx
create mode 100644 packages/appstore/src/components/demo-reset-balance/index.ts
create mode 100644 packages/appstore/src/components/empty-state/__tests__/empty-state.spec.tsx
create mode 100644 packages/appstore/src/components/empty-state/empty-state.scss
create mode 100644 packages/appstore/src/components/empty-state/empty-state.tsx
create mode 100644 packages/appstore/src/components/empty-state/index.ts
create mode 100644 packages/appstore/src/components/eu-disclaimer/__tests__/eu-disclaimer.spec.tsx
create mode 100644 packages/appstore/src/components/eu-disclaimer/eu-disclaimer.scss
create mode 100644 packages/appstore/src/components/eu-disclaimer/eu-disclaimer.tsx
create mode 100644 packages/appstore/src/components/eu-disclaimer/index.ts
create mode 100644 packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.scss
create mode 100644 packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.tsx
create mode 100644 packages/appstore/src/components/get-more-wallet-card/index.ts
create mode 100644 packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-body.spec.tsx
create mode 100644 packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal-header.spec.tsx
create mode 100644 packages/appstore/src/components/modals/wallet-modal/__tests__/wallet-modal.spec.tsx
create mode 100644 packages/appstore/src/components/modals/wallet-modal/index.ts
create mode 100644 packages/appstore/src/components/modals/wallet-modal/provider.tsx
create mode 100644 packages/appstore/src/components/modals/wallet-modal/wallet-modal-body.tsx
create mode 100644 packages/appstore/src/components/modals/wallet-modal/wallet-modal-header.tsx
create mode 100644 packages/appstore/src/components/modals/wallet-modal/wallet-modal.scss
create mode 100644 packages/appstore/src/components/modals/wallet-modal/wallet-modal.tsx
create mode 100644 packages/appstore/src/components/toggle-account-type/index.ts
create mode 100644 packages/appstore/src/components/toggle-account-type/stories/toggle-account-type.stories.tsx
create mode 100644 packages/appstore/src/components/toggle-account-type/toggle-account-type.tsx
create mode 100644 packages/appstore/src/components/transaction-list/index.ts
create mode 100644 packages/appstore/src/components/transaction-list/non-pending-transaction.tsx
create mode 100644 packages/appstore/src/components/transaction-list/transaction-for-day.tsx
create mode 100644 packages/appstore/src/components/transaction-list/transaction-list.scss
create mode 100644 packages/appstore/src/components/transaction-list/transaction-list.tsx
create mode 100644 packages/appstore/src/components/wallet-button/__tests__/wallet-button.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-button/index.ts
create mode 100644 packages/appstore/src/components/wallet-button/wallet-button.scss
create mode 100644 packages/appstore/src/components/wallet-button/wallet-button.tsx
create mode 100644 packages/appstore/src/components/wallet-cards-carousel/__tests__/wallet-cards-carousel.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-cards-carousel/cards-slider-swiper.tsx
create mode 100644 packages/appstore/src/components/wallet-cards-carousel/index.ts
create mode 100644 packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.scss
create mode 100644 packages/appstore/src/components/wallet-cards-carousel/wallet-cards-carousel.tsx
create mode 100644 packages/appstore/src/components/wallet-content/__tests__/wallet-content-divider.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-content/__tests__/wallet-content.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-content/__tests__/wallet-transfer-block.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-content/index.ts
create mode 100644 packages/appstore/src/components/wallet-content/wallet-cfds-listing.tsx
create mode 100644 packages/appstore/src/components/wallet-content/wallet-content-divider.tsx
create mode 100644 packages/appstore/src/components/wallet-content/wallet-content.scss
create mode 100644 packages/appstore/src/components/wallet-content/wallet-content.tsx
create mode 100644 packages/appstore/src/components/wallet-content/wallet-option-multipliers-listing.tsx
create mode 100644 packages/appstore/src/components/wallet-content/wallet-transfer-block.tsx
create mode 100644 packages/appstore/src/components/wallet-deposit/index.ts
create mode 100644 packages/appstore/src/components/wallet-deposit/wallet-deposit.scss
create mode 100644 packages/appstore/src/components/wallet-deposit/wallet-deposit.tsx
create mode 100644 packages/appstore/src/components/wallet-header/__tests__/wallet-header.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-header/index.ts
create mode 100644 packages/appstore/src/components/wallet-header/wallet-currency-card.tsx
create mode 100644 packages/appstore/src/components/wallet-header/wallet-header-balance.tsx
create mode 100644 packages/appstore/src/components/wallet-header/wallet-header-buttons.tsx
create mode 100644 packages/appstore/src/components/wallet-header/wallet-header-title.tsx
create mode 100644 packages/appstore/src/components/wallet-header/wallet-header.scss
create mode 100644 packages/appstore/src/components/wallet-header/wallet-header.tsx
create mode 100644 packages/appstore/src/components/wallet-jurisdiction-badge/__tests__/wallet-jurisdiction-badge.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-jurisdiction-badge/index.ts
create mode 100644 packages/appstore/src/components/wallet-jurisdiction-badge/wallet-jurisdiction-badge.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/__tests__/wallet-transfer.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/index.ts
create mode 100644 packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-list.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/transfer-account-selector/__tests__/transfer-account-selector.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/transfer-account-selector/index.ts
create mode 100644 packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-list.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.scss
create mode 100644 packages/appstore/src/components/wallet-transfer/transfer-account-selector/transfer-account-selector.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/__tests__/wallet-transfer-tile.spec.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/index.ts
create mode 100644 packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.scss
create mode 100644 packages/appstore/src/components/wallet-transfer/wallet-transfer-tile/wallet-transfer-tile.tsx
create mode 100644 packages/appstore/src/components/wallet-transfer/wallet-transfer.scss
create mode 100644 packages/appstore/src/components/wallet-transfer/wallet-transfer.tsx
create mode 100644 packages/appstore/src/components/wallet-withdrawal/__tests__/wallet-withdrawal.test.tsx
create mode 100644 packages/appstore/src/components/wallet-withdrawal/index.ts
create mode 100644 packages/appstore/src/components/wallet-withdrawal/wallet-withdrawal.tsx
create mode 100644 packages/appstore/src/constants/wallet-labels.ts
create mode 100644 packages/appstore/src/constants/wallet-mocked-response.ts
create mode 100644 packages/appstore/src/constants/wallet_description_mapper.ts
create mode 100644 packages/appstore/src/helpers/index.js
delete mode 100644 packages/appstore/src/helpers/index.ts
delete mode 100644 packages/appstore/src/helpers/total-assets-helper.ts
create mode 100644 packages/appstore/src/modules/Page404/index.tsx
create mode 100644 packages/appstore/src/modules/wallets/desktop-wallets-list.tsx
create mode 100644 packages/appstore/src/modules/wallets/index.ts
create mode 100644 packages/appstore/src/modules/wallets/mobile-wallets-carousel.tsx
create mode 100644 packages/appstore/src/modules/wallets/wallets.scss
create mode 100644 packages/appstore/src/modules/wallets/wallets.tsx
diff --git a/packages/appstore/package.json b/packages/appstore/package.json
index a4a5928f9293..6e58ffee364a 100644
--- a/packages/appstore/package.json
+++ b/packages/appstore/package.json
@@ -25,6 +25,7 @@
"license": "Apache-2.0",
"dependencies": {
"@deriv-com/analytics": "1.4.13",
+ "@deriv/api": "^1.0.0",
"@deriv/account": "^1.0.0",
"@deriv/api-types": "^1.0.172",
"@deriv/cashier": "^1.0.0",
@@ -32,16 +33,26 @@
"@deriv/components": "^1.0.0",
"@deriv/shared": "^1.0.0",
"@deriv/stores": "^1.0.0",
+ "@deriv/trader": "^3.8.0",
"@deriv/translations": "^1.0.0",
+ "@deriv/utils": "^1.0.0",
+ "@testing-library/jest-dom": "^5.12.0",
"@deriv/hooks": "^1.0.0",
+ "@deriv/wallets": "^1.0.0",
"classnames": "^2.2.6",
+ "formik": "^2.1.4",
+ "lodash.debounce": "^4.0.8",
"mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0",
+ "object.fromentries": "^2.0.0",
+ "prop-types": "^15.7.2",
"react": "^17.0.2",
"react-content-loader": "^6.2.0",
"react-joyride": "^2.5.3",
"react-router": "^5.2.0",
- "react-router-dom": "^5.2.0"
+ "react-router-dom": "^5.2.0",
+ "react-transition-group": "4.4.2",
+ "embla-carousel-react": "8.0.0-rc12"
},
"devDependencies": {
"@babel/eslint-parser": "^7.17.0",
@@ -54,8 +65,8 @@
"@storybook/manager-webpack5": "^6.5.10",
"@storybook/react": "^6.5.10",
"@testing-library/react": "^12.0.0",
- "@testing-library/jest-dom": "^5.12.0",
"@testing-library/user-event": "^13.5.0",
+ "@types/object.fromentries": "^2.0.0",
"@types/react": "^18.0.7",
"@types/react-dom": "^18.0.0",
"@types/react-router-dom": "^5.1.6",
diff --git a/packages/appstore/src/components/add-more-wallets/__test__/add-more-wallets.spec.tsx b/packages/appstore/src/components/add-more-wallets/__test__/add-more-wallets.spec.tsx
new file mode 100644
index 000000000000..5ae97d7ff1ad
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/__test__/add-more-wallets.spec.tsx
@@ -0,0 +1,136 @@
+import React from 'react';
+import { screen, render } from '@testing-library/react';
+import { APIProvider } from '@deriv/api';
+import { StoreProvider, mockStore } from '@deriv/stores';
+import AddMoreWallets from '../add-more-wallets';
+
+jest.mock('../wallet-add-card', () => {
+ const AddWalletCard = () =>
AddWalletCard
;
+ return AddWalletCard;
+});
+
+jest.mock('../carousel-container', () => {
+ const CarouselContainer = ({ children }: React.PropsWithChildren) => (
+ {children}
+ );
+ return CarouselContainer;
+});
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useFetch: jest.fn((name: string) => {
+ if (name === 'authorize') {
+ return {
+ data: {
+ authorize: {
+ account_list: [
+ { account_category: 'wallet', landing_company_name: 'svg', is_virtual: 0, currency: 'USD' },
+ ],
+ landing_company_name: 'svg',
+ },
+ },
+ };
+ }
+
+ if (name === 'get_account_types') {
+ return {
+ data: {
+ get_account_types: {
+ wallet: {
+ crypto: {
+ currencies: ['BTC', 'ETH'],
+ },
+ doughflow: {
+ currencies: ['USD', 'EUR'],
+ },
+ },
+ },
+ },
+ };
+ }
+
+ return { data: undefined };
+ }),
+}));
+
+describe('AddMoreWallets', () => {
+ const wrapper = (mock: ReturnType) => {
+ const Component = ({ children }: React.PropsWithChildren) => {
+ return (
+
+ {children}
+
+ );
+ };
+ return Component;
+ };
+
+ it('should render the component without errors', () => {
+ const mock = mockStore({
+ client: {
+ loginid: 'CRW909900',
+ accounts: {
+ CRW909900: {
+ token: '12345',
+ },
+ },
+ is_crypto: (currency: string) => currency === 'BTC',
+ },
+ });
+
+ const { container } = render(, { wrapper: wrapper(mock) });
+ expect(container).toBeInTheDocument();
+ });
+
+ it('should render the title correctly', () => {
+ const mock = mockStore({
+ client: {
+ loginid: 'CRW909900',
+ accounts: {
+ CRW909900: {
+ token: '12345',
+ },
+ },
+ is_crypto: (currency: string) => currency === 'BTC',
+ },
+ });
+
+ render(, { wrapper: wrapper(mock) });
+ expect(screen.getByText('Add more Wallets')).toBeInTheDocument();
+ });
+
+ it('should render the carousel', () => {
+ const mock = mockStore({
+ client: {
+ loginid: 'CRW909900',
+ accounts: {
+ CRW909900: {
+ token: '12345',
+ },
+ },
+ is_crypto: (currency: string) => currency === 'BTC',
+ },
+ });
+
+ render(, { wrapper: wrapper(mock) });
+ expect(screen.getByTestId('dt_carousel_container')).toBeInTheDocument();
+ });
+
+ it('should render the wallet add card', () => {
+ const mock = mockStore({
+ client: {
+ loginid: 'CRW909900',
+ accounts: {
+ CRW909900: {
+ token: '12345',
+ },
+ },
+ is_crypto: (currency: string) => currency === 'BTC',
+ },
+ });
+
+ render(, { wrapper: wrapper(mock) });
+ const wallet_cards = screen.getAllByText(/AddWalletCard/i);
+ expect(wallet_cards).toHaveLength(4);
+ });
+});
diff --git a/packages/appstore/src/components/add-more-wallets/__test__/wallet-add-card.spec.tsx b/packages/appstore/src/components/add-more-wallets/__test__/wallet-add-card.spec.tsx
new file mode 100644
index 000000000000..8666b2da0b61
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/__test__/wallet-add-card.spec.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { APIProvider } from '@deriv/api';
+import { screen, render } from '@testing-library/react';
+import { StoreProvider, mockStore } from '@deriv/stores';
+import AddWalletCard from '../wallet-add-card';
+
+const wallet_info = {
+ currency: 'BTC',
+ gradient_card_class: '',
+ landing_company_name: 'svg',
+ is_added: false,
+};
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useFetch: jest.fn((name: string) => {
+ if (name === 'authorize') {
+ return {
+ data: {
+ authorize: {
+ account_list: [
+ { account_category: 'wallet', landing_company_name: 'svg', is_virtual: 0, currency: 'USD' },
+ ],
+ landing_company_name: 'svg',
+ },
+ },
+ };
+ }
+
+ if (name === 'get_account_types') {
+ return {
+ data: {
+ get_account_types: {
+ wallet: {
+ crypto: {
+ currencies: ['BTC'],
+ },
+ doughflow: {
+ currencies: ['USD'],
+ },
+ },
+ },
+ },
+ };
+ }
+
+ if (name === 'website_status') {
+ return {
+ data: {
+ website_status: {
+ currencies_config: {
+ USD: { type: 'fiat', name: 'US Dollar' },
+ BTC: { type: 'crypto', name: 'Bitcoin' },
+ UST: { type: 'crypto', name: 'USDT' },
+ },
+ },
+ },
+ };
+ }
+
+ return { data: undefined };
+ }),
+}));
+
+describe('AddWalletCard', () => {
+ it('should render currency card', () => {
+ const mock = mockStore({});
+
+ render(
+
+
+
+
+
+ );
+
+ const add_btn = screen.getByRole('button', { name: /Add/i });
+ expect(screen.getByText('BTC Wallet')).toBeInTheDocument();
+ expect(screen.getByText('SVG')).toBeInTheDocument();
+ expect(add_btn).toBeInTheDocument();
+ expect(add_btn).toBeEnabled();
+ expect(
+ screen.getByText(
+ "Deposit and withdraw Bitcoin, the world's most popular cryptocurrency, hosted on the Bitcoin blockchain."
+ )
+ ).toBeInTheDocument();
+ });
+
+ it('should disabled button when it is disabled', () => {
+ const mock = mockStore({});
+
+ render(
+
+
+
+
+
+ );
+
+ const added_btn = screen.getByRole('button', { name: /Added/i });
+ expect(added_btn).toBeInTheDocument();
+ expect(added_btn).toBeDisabled();
+ });
+
+ it('should show USDT instead of UST for UST currency', () => {
+ const mock = mockStore({});
+
+ render(
+
+
+
+
+
+ );
+ expect(screen.getByText('USDT Wallet')).toBeInTheDocument();
+ expect(screen.queryByText('UST Wallet')).not.toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/add-more-wallets/add-more-wallets.scss b/packages/appstore/src/components/add-more-wallets/add-more-wallets.scss
new file mode 100644
index 000000000000..a9a60392bb33
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/add-more-wallets.scss
@@ -0,0 +1,91 @@
+@mixin align-center {
+ display: flex;
+ align-items: center;
+}
+
+.add-wallets {
+ display: flex;
+ padding-top: 2.4rem;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 1.6rem;
+ width: 100%;
+
+ &__title {
+ @include mobile {
+ padding-left: 1.6rem;
+ }
+ }
+ &__content {
+ background-color: var(--general-main-1);
+ border-radius: $BORDER_RADIUS;
+ position: relative;
+ width: 100%;
+ }
+ &__card {
+ width: 23rem;
+ height: 29rem;
+ padding: 1.6rem;
+ border-radius: 1.6rem;
+ border: 0.1rem solid var(--general-active);
+ background-color: var(--general-main-1);
+ box-shadow: $wallet-box-shadow;
+ cursor: pointer;
+ &-description,
+ &-wrapper {
+ display: flex;
+ flex-direction: column;
+ }
+ &-description {
+ align-items: flex-start;
+ gap: 0.8rem;
+ }
+ &-wrapper {
+ align-items: center;
+ gap: 2.4rem;
+ @include mobile {
+ gap: 1.6rem;
+ }
+ }
+ }
+}
+
+.carousel {
+ overflow: hidden;
+ padding: 3.2rem 1.6rem;
+ &__wrapper {
+ height: 100%;
+ @include align-center;
+ justify-content: flex-start;
+ gap: 2.4rem;
+ }
+ &__btn {
+ background-color: var(--general-main-1);
+ z-index: 1;
+ color: var(--text-general);
+ position: absolute;
+ @include align-center;
+ justify-content: center;
+ top: 45%;
+ cursor: pointer;
+ width: 4rem;
+ height: 4rem;
+ border: 1px solid var(--general-background-main);
+ border-radius: 50%;
+ box-shadow: $btn-shadow;
+ &-prev {
+ left: 1.6rem;
+ }
+ &-next {
+ right: 1.6rem;
+ }
+ &-icon {
+ width: 50%;
+ height: 35%;
+ }
+ &:disabled {
+ opacity: 0.3;
+ display: none;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/add-more-wallets/add-more-wallets.tsx b/packages/appstore/src/components/add-more-wallets/add-more-wallets.tsx
new file mode 100644
index 000000000000..0af06208e35e
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/add-more-wallets.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Text, Loading } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import { useAvailableWallets } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import CarouselContainer from './carousel-container';
+import AddWalletCard from './wallet-add-card';
+
+import './add-more-wallets.scss';
+
+const AddMoreWallets = observer(() => {
+ const { data, isLoading } = useAvailableWallets();
+ const {
+ ui: { is_mobile },
+ } = useStore();
+
+ return (
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ data?.map(wallet => )
+ )}
+
+
+ );
+});
+
+export default AddMoreWallets;
diff --git a/packages/appstore/src/components/add-more-wallets/carousel-buttons.tsx b/packages/appstore/src/components/add-more-wallets/carousel-buttons.tsx
new file mode 100644
index 000000000000..2e3353819d72
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/carousel-buttons.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Icon } from '@deriv/components';
+
+type PrevNextButtonProps = {
+ enabled: boolean;
+ nav_action: 'prev' | 'next';
+ onClick: () => void;
+};
+
+export const CarouselButton: React.FC = props => {
+ const { enabled, onClick, nav_action } = props;
+ const icon = nav_action === 'prev' ? 'IcChevronLeftBold' : 'IcChevronRightBold';
+ const className = nav_action === 'prev' ? 'carousel__btn carousel__btn-prev' : 'carousel__btn carousel__btn-next';
+
+ return (
+
+ );
+};
diff --git a/packages/appstore/src/components/add-more-wallets/carousel-container.tsx b/packages/appstore/src/components/add-more-wallets/carousel-container.tsx
new file mode 100644
index 000000000000..5ce8f9b059f2
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/carousel-container.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import useEmblaCarousel, { EmblaCarouselType, EmblaOptionsType } from 'embla-carousel-react';
+import { observer, useStore } from '@deriv/stores';
+import { CarouselButton } from './carousel-buttons';
+
+const CarouselContainer: React.FC> = observer(({ children }) => {
+ const { ui } = useStore();
+ const { is_mobile } = ui;
+
+ const options: EmblaOptionsType = {
+ align: 0,
+ containScroll: 'trimSnaps',
+ watchDrag: is_mobile,
+ };
+
+ const [emblaRef, emblaApi] = useEmblaCarousel(options);
+ const [is_hovered, setIsHovered] = React.useState(is_mobile);
+ const [prev_btn_enabled, setPrevBtnEnabled] = React.useState(false);
+ const [next_btn_enabled, setNextBtnEnabled] = React.useState(false);
+
+ const scrollPrev = React.useCallback(() => emblaApi?.scrollPrev(), [emblaApi]);
+ const scrollNext = React.useCallback(() => emblaApi?.scrollNext(), [emblaApi]);
+
+ const onSelect = React.useCallback((embla_api: EmblaCarouselType) => {
+ setPrevBtnEnabled(embla_api.canScrollPrev());
+ setNextBtnEnabled(embla_api.canScrollNext());
+ }, []);
+
+ React.useEffect(() => {
+ if (!emblaApi) return;
+
+ onSelect(emblaApi);
+ emblaApi.on('reInit', onSelect);
+ emblaApi.on('select', onSelect);
+ }, [emblaApi, onSelect]);
+
+ return (
+ !is_mobile && setIsHovered(true)}
+ onMouseLeave={() => !is_mobile && setIsHovered(false)}
+ >
+
+ {!is_mobile && is_hovered && (
+
+
+
+
+ )}
+
+ );
+});
+
+export default CarouselContainer;
diff --git a/packages/appstore/src/components/add-more-wallets/index.ts b/packages/appstore/src/components/add-more-wallets/index.ts
new file mode 100644
index 000000000000..db03122683b1
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/index.ts
@@ -0,0 +1,3 @@
+import AddMoreWallets from './add-more-wallets';
+
+export default AddMoreWallets;
diff --git a/packages/appstore/src/components/add-more-wallets/wallet-add-card.tsx b/packages/appstore/src/components/add-more-wallets/wallet-add-card.tsx
new file mode 100644
index 000000000000..5e74b263ee52
--- /dev/null
+++ b/packages/appstore/src/components/add-more-wallets/wallet-add-card.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { TWalletInfo } from 'Types';
+import { Text, WalletCard } from '@deriv/components';
+import { useCurrencyConfig } from '@deriv/hooks';
+import { observer, useStore } from '@deriv/stores';
+import { Localize } from '@deriv/translations';
+import { getWalletCurrencyIcon } from '@deriv/utils';
+import wallet_description_mapper from 'Constants/wallet_description_mapper';
+
+type TAddWalletCard = {
+ wallet_info: React.PropsWithChildren;
+};
+
+const AddWalletCard = observer(({ wallet_info }: TAddWalletCard) => {
+ const {
+ ui: { is_dark_mode_on, is_mobile },
+ } = useStore();
+
+ const { currency = '', landing_company_name, is_added, gradient_card_class } = wallet_info;
+ const { getConfig } = useCurrencyConfig();
+ const currency_config = getConfig(currency);
+
+ const wallet_details = {
+ currency,
+ icon: getWalletCurrencyIcon(currency, is_dark_mode_on),
+ icon_type: currency_config?.type,
+ jurisdiction_title: landing_company_name?.toUpperCase(),
+ name: currency_config?.name,
+ gradient_class: gradient_card_class,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {wallet_description_mapper[currency]}
+
+
+
+
+ );
+});
+
+export default AddWalletCard;
diff --git a/packages/appstore/src/components/containers/__tests__/wallets.spec.tsx b/packages/appstore/src/components/containers/__tests__/wallets.spec.tsx
new file mode 100644
index 000000000000..f2e97f99c0ca
--- /dev/null
+++ b/packages/appstore/src/components/containers/__tests__/wallets.spec.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import Wallet from '../wallet';
+import { TWalletAccount } from 'Types';
+
+const mockedRootStore = mockStore({});
+
+jest.mock('react-transition-group', () => ({
+ CSSTransition: jest.fn(({ children }) => {children}
),
+}));
+
+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() })),
+}));
+
+jest.mock('./../currency-switcher-container', () => jest.fn(({ children }) => {children}
));
+jest.mock('./../../wallet-content', () => jest.fn(() => wallet test content));
+
+describe('', () => {
+ let mocked_props: TWalletAccount;
+ beforeEach(() => {
+ // @ts-expect-error need to give a value to all props
+ mocked_props = {
+ currency: 'USD',
+ landing_company_name: 'svg',
+ balance: 10000,
+ loginid: 'CRW123123',
+ is_selected: false,
+ is_demo: true,
+ is_malta_wallet: false,
+ gradient_header_class: 'wallet-header__usd-bg',
+ gradient_card_class: 'wallet-card__usd-bg',
+ };
+ });
+ it('Check class for NOT demo', () => {
+ const { container } = render(
+
+
+
+ );
+ expect(container.childNodes[0]).toHaveClass('wallet');
+ expect(container.childNodes[0]).not.toHaveClass('wallet__demo');
+ });
+
+ it('Check class for demo', () => {
+ const { container } = render(
+
+
+
+ );
+
+ expect(container.childNodes[0]).toHaveClass('wallet');
+ expect(container.childNodes[0]).toHaveClass('wallet__demo');
+ });
+
+ it('Check for demo wallet header', () => {
+ render(
+
+
+
+ );
+
+ const currency_card = screen.queryByTestId('dt_demo');
+ expect(currency_card).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/containers/listing-container.tsx b/packages/appstore/src/components/containers/listing-container.tsx
index 2bba1e811121..969cec68f38a 100644
--- a/packages/appstore/src/components/containers/listing-container.tsx
+++ b/packages/appstore/src/components/containers/listing-container.tsx
@@ -5,6 +5,7 @@ import { TWalletAccount } from 'Types';
import CurrencySwitcherCard from 'Components/currency-switcher-card';
import GridContainer from 'Components/containers/grid-container';
import TitleCardLoader from 'Components/pre-loader/title-card-loader';
+import WalletTransferBlock from 'Components/wallet-content/wallet-transfer-block';
import './listing-container.scss';
type TListingContainerProps = {
@@ -35,8 +36,9 @@ const Options = observer(({ title, description, is_deriv_platform }: TOptionsPro
return ;
});
-const Switcher = ({ is_deriv_platform }: TSwitcherProps) => {
+const Switcher = ({ wallet_account, is_deriv_platform }: TSwitcherProps) => {
if (!is_deriv_platform) return null;
+ if (wallet_account) return ;
return ;
};
diff --git a/packages/appstore/src/components/containers/wallet.scss b/packages/appstore/src/components/containers/wallet.scss
new file mode 100644
index 000000000000..312f6136b87a
--- /dev/null
+++ b/packages/appstore/src/components/containers/wallet.scss
@@ -0,0 +1,44 @@
+.wallet {
+ background-color: var(--general-main-1);
+ border-radius: $BORDER_RADIUS * 4;
+ align-self: stretch;
+
+ &__demo {
+ background-color: var(--wallet-demo-bg-color);
+ }
+
+ &__content {
+ padding: 2.4rem;
+ }
+
+ &__content-transition {
+ &-enter {
+ transform: translateY(-3rem);
+ opacity: 0;
+ }
+
+ &-enter-active {
+ transition: all 240ms ease-in-out;
+ transform: translateY(0);
+ position: relative;
+ opacity: 1;
+ }
+
+ &-enter-done,
+ &-exit {
+ transform: translateY(0);
+ opacity: 1;
+ }
+
+ &-exit-active {
+ transition: all 240ms ease-in-out;
+ transform: translateY(-3rem);
+ position: relative;
+ opacity: 0;
+ }
+
+ &-exit-done {
+ opacity: 0;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/containers/wallet.tsx b/packages/appstore/src/components/containers/wallet.tsx
new file mode 100644
index 000000000000..895a73c68e16
--- /dev/null
+++ b/packages/appstore/src/components/containers/wallet.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import classNames from 'classnames';
+import WalletHeader from 'Components/wallet-header';
+import WalletContent from 'Components/wallet-content';
+import { CSSTransition } from 'react-transition-group';
+import { TWalletAccount } from 'Types';
+import './wallet.scss';
+
+type TWallet = {
+ wallet_account: TWalletAccount;
+};
+
+const Wallet = ({ wallet_account }: TWallet) => {
+ const headerRef = React.useRef(null);
+ const { is_selected, is_demo, is_malta_wallet } = wallet_account;
+
+ return (
+
+
+ {
+ if (headerRef?.current) {
+ headerRef.current.style.scrollMargin = '20px';
+ headerRef.current.scrollIntoView({ behavior: 'smooth' });
+ }
+ }}
+ classNames='wallet__content-transition'
+ unmountOnExit
+ >
+
+
+
+ );
+};
+
+export default Wallet;
diff --git a/packages/appstore/src/components/demo-reset-balance/__tests__/demo-reset-balance.spec.tsx b/packages/appstore/src/components/demo-reset-balance/__tests__/demo-reset-balance.spec.tsx
new file mode 100644
index 000000000000..e0f4c1ee1e16
--- /dev/null
+++ b/packages/appstore/src/components/demo-reset-balance/__tests__/demo-reset-balance.spec.tsx
@@ -0,0 +1,135 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { APIProvider, useRequest } from '@deriv/api';
+import { StoreProvider, mockStore } from '@deriv/stores';
+import DemoResetBalance from '../demo-reset-balance';
+
+jest.mock('@deriv/api', () => ({
+ ...jest.requireActual('@deriv/api'),
+ useRequest: jest.fn(() => ({ mutate: jest.fn() })),
+}));
+
+const mockUseRequest = useRequest as jest.MockedFunction>;
+
+describe('', () => {
+ it('should render demo reset balance component correctly', () => {
+ const mock = mockStore({});
+ // @ts-expect-error need to come up with a way to mock the return type of useRequest
+ mockUseRequest.mockReturnValue({});
+ const setActiveTabIndex = jest.fn();
+
+ render(, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ expect(screen.getByText('Reset balance to 10,000.00 USD')).toBeInTheDocument();
+ expect(
+ screen.getByText('Reset your virtual balance if it falls below 10,000.00 USD or exceeds 10,000.00 USD.')
+ ).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Reset balance' })).toBeInTheDocument();
+ });
+
+ it('should disable reset balance button if the balance is equal to 10000 usd', () => {
+ const mock = mockStore({
+ client: {
+ accounts: {
+ VRW1002: {
+ balance: 10000,
+ },
+ },
+ loginid: 'VRW1002',
+ },
+ });
+ // @ts-expect-error need to come up with a way to mock the return type of useRequest
+ mockUseRequest.mockReturnValue({});
+ const setActiveTabIndex = jest.fn();
+
+ render(, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ expect(screen.getByRole('button', { name: /Reset balance/i })).toBeDisabled();
+ });
+
+ it('should call reset balance API when click on Reset balance', () => {
+ const mock = mockStore({
+ client: {
+ accounts: {
+ VRW1002: {
+ balance: 9880,
+ },
+ },
+ loginid: 'VRW1002',
+ },
+ });
+ // @ts-expect-error need to come up with a way to mock the return type of useRequest
+ mockUseRequest.mockReturnValue({
+ mutate: jest.fn(),
+ });
+ const { mutate } = mockUseRequest('topup_virtual');
+ const setActiveTabIndex = jest.fn();
+
+ render(, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ const reset_balance_button = screen.getByRole('button', { name: /Reset balance/i });
+ userEvent.click(reset_balance_button);
+ expect(mutate).toBeCalledTimes(1);
+ });
+
+ it('should change tab when click on transfer funds button', () => {
+ const mock = mockStore({});
+ // @ts-expect-error need to come up with a way to mock the return type of useRequest
+ mockUseRequest.mockReturnValue({
+ isSuccess: true,
+ });
+ const setActiveTabIndex = jest.fn();
+
+ render(, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ const transfer_funds_button = screen.getByRole('button', { name: /Transfer funds/i });
+
+ userEvent.click(transfer_funds_button);
+ expect(setActiveTabIndex).toBeCalledTimes(1);
+ });
+
+ it('should show success message and transfer funds button if reset balance is reset successfully', () => {
+ const mock = mockStore({});
+ // @ts-expect-error need to come up with a way to mock the return type of useRequest
+ mockUseRequest.mockReturnValue({
+ isSuccess: true,
+ });
+ const setActiveTabIndex = jest.fn();
+
+ render(, {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ expect(screen.getByText('Your balance has been reset to 10,000.00 USD.')).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /Transfer funds/i })).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/demo-reset-balance/demo-reset-balance.scss b/packages/appstore/src/components/demo-reset-balance/demo-reset-balance.scss
new file mode 100644
index 000000000000..82243f29aec9
--- /dev/null
+++ b/packages/appstore/src/components/demo-reset-balance/demo-reset-balance.scss
@@ -0,0 +1,20 @@
+.reset-balance {
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+
+ &__title,
+ &__button {
+ margin-top: 2.4rem;
+
+ @include mobile {
+ margin-top: 1.6rem;
+ }
+ }
+
+ &__text {
+ margin-top: 0.8rem;
+ }
+}
diff --git a/packages/appstore/src/components/demo-reset-balance/demo-reset-balance.tsx b/packages/appstore/src/components/demo-reset-balance/demo-reset-balance.tsx
new file mode 100644
index 000000000000..3294963d2d58
--- /dev/null
+++ b/packages/appstore/src/components/demo-reset-balance/demo-reset-balance.tsx
@@ -0,0 +1,82 @@
+import React from 'react';
+import { Icon, Text, Button, Div100vhContainer } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import { useRequest } from '@deriv/api';
+import { observer, useStore } from '@deriv/stores';
+import './demo-reset-balance.scss';
+
+type TDemoResetBalanceProps = {
+ setActiveTabIndex?: (index: number) => void;
+};
+
+const DemoResetBalance = observer(({ setActiveTabIndex }: TDemoResetBalanceProps) => {
+ const { mutate, isSuccess: isResetBalanceSuccess } = useRequest('topup_virtual');
+ const { client, ui } = useStore();
+ const { accounts, loginid } = client;
+ const { is_mobile } = ui;
+
+ const can_reset_balance = loginid && (accounts[loginid]?.balance || 0) !== 10000;
+
+ const resetBalance = () => {
+ mutate();
+ };
+
+ const redirectToTransferTab = () => {
+ setActiveTabIndex?.(0);
+ };
+
+ return (
+
+
+
+
+
+ {isResetBalanceSuccess ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isResetBalanceSuccess ? (
+
+ ) : (
+
+ )}
+
+
+ {isResetBalanceSuccess ? (
+
+ ) : (
+
+ )}
+
+
+ );
+});
+
+export default DemoResetBalance;
diff --git a/packages/appstore/src/components/demo-reset-balance/index.ts b/packages/appstore/src/components/demo-reset-balance/index.ts
new file mode 100644
index 000000000000..0bb492bb7d66
--- /dev/null
+++ b/packages/appstore/src/components/demo-reset-balance/index.ts
@@ -0,0 +1,3 @@
+import DemoResetBalance from './demo-reset-balance';
+
+export default DemoResetBalance;
diff --git a/packages/appstore/src/components/empty-state/__tests__/empty-state.spec.tsx b/packages/appstore/src/components/empty-state/__tests__/empty-state.spec.tsx
new file mode 100644
index 000000000000..64e03e7ceefc
--- /dev/null
+++ b/packages/appstore/src/components/empty-state/__tests__/empty-state.spec.tsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import EmptyState from '../empty-state';
+import { render, screen } from '@testing-library/react';
+
+describe('EmptyState', () => {
+ it('should render correctly', () => {
+ const { container } = render(
+ (
+ <>
+ Message
+ Some content
+ >
+ )}
+ renderTitle={() => Title
}
+ />
+ );
+ expect(container).toBeInTheDocument();
+ });
+
+ it('should render correctly with the correct text', () => {
+ const { container } = render(
+ (
+ <>
+ Message
+ Some content
+ >
+ )}
+ renderTitle={() => Title
}
+ />
+ );
+
+ expect(container).toBeInTheDocument();
+ expect(screen.getByText('Message')).toBeInTheDocument();
+ expect(screen.getByText('Some content')).toBeInTheDocument();
+ expect(screen.getByText('Title')).toBeInTheDocument();
+ });
+});
diff --git a/packages/appstore/src/components/empty-state/empty-state.scss b/packages/appstore/src/components/empty-state/empty-state.scss
new file mode 100644
index 000000000000..aef48dbd92a3
--- /dev/null
+++ b/packages/appstore/src/components/empty-state/empty-state.scss
@@ -0,0 +1,14 @@
+.dw-empty-state {
+ margin-top: 5.6rem;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ &__content {
+ margin-top: 2.4rem;
+
+ & :last-child {
+ margin-top: 0.8rem;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/empty-state/empty-state.tsx b/packages/appstore/src/components/empty-state/empty-state.tsx
new file mode 100644
index 000000000000..c8e653a48b84
--- /dev/null
+++ b/packages/appstore/src/components/empty-state/empty-state.tsx
@@ -0,0 +1,26 @@
+import * as React from 'react';
+import { Icon, Text } from '@deriv/components';
+
+type TProps = {
+ icon_name: string;
+ renderMessage: () => React.ReactNode;
+ renderTitle: () => React.ReactNode;
+};
+
+const EmptyState = ({ icon_name, renderMessage, renderTitle }: TProps) => {
+ return (
+
+
+
+
+ {renderTitle()}
+
+
+ {renderMessage()}
+
+
+
+ );
+};
+
+export default EmptyState;
diff --git a/packages/appstore/src/components/empty-state/index.ts b/packages/appstore/src/components/empty-state/index.ts
new file mode 100644
index 000000000000..8e05d110573b
--- /dev/null
+++ b/packages/appstore/src/components/empty-state/index.ts
@@ -0,0 +1,4 @@
+import EmptyState from './empty-state';
+import './empty-state.scss';
+
+export default EmptyState;
diff --git a/packages/appstore/src/components/eu-disclaimer/__tests__/eu-disclaimer.spec.tsx b/packages/appstore/src/components/eu-disclaimer/__tests__/eu-disclaimer.spec.tsx
new file mode 100644
index 000000000000..cab7453ebbf8
--- /dev/null
+++ b/packages/appstore/src/components/eu-disclaimer/__tests__/eu-disclaimer.spec.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import { mockStore, StoreProvider } from '@deriv/stores';
+import EUDisclaimer from '../eu-disclaimer';
+
+const mockedRootStore = mockStore({});
+
+describe('', () => {
+ it('Check disclaimer exists', () => {
+ render(
+
+
+
+ );
+
+ const eu_statutory_disclaimer = screen.getByText('EU statutory disclaimer');
+
+ expect(eu_statutory_disclaimer).toBeInTheDocument();
+ });
+
+ it('Check disclaimer for wallets exists', () => {
+ render(
+
+
+
+ );
+
+ const eu_statutory_disclaimer = screen.queryByText('EU statutory disclaimer');
+
+ expect(eu_statutory_disclaimer).not.toBeInTheDocument();
+ });
+
+ it('Check classes when dont pass the props', () => {
+ render(
+
+
+
+ );
+
+ const wrapper = screen.getByTestId('dt_disclaimer_wrapper');
+ const text = screen.getByTestId('dt_disclaimer_text');
+
+ expect(wrapper).toHaveClass('disclaimer');
+ expect(text).toHaveClass('disclaimer-text');
+ });
+
+ it('Check classes when pass the props', () => {
+ render(
+
+
+
+ );
+
+ const wrapper = screen.getByTestId('dt_disclaimer_wrapper');
+ const text = screen.getByTestId('dt_disclaimer_text');
+
+ expect(wrapper).not.toHaveClass('disclaimer');
+ expect(text).not.toHaveClass('disclaimer-text');
+ expect(wrapper).toHaveClass('wrapper-class');
+ expect(text).toHaveClass('text-class');
+ });
+});
diff --git a/packages/appstore/src/components/eu-disclaimer/eu-disclaimer.scss b/packages/appstore/src/components/eu-disclaimer/eu-disclaimer.scss
new file mode 100644
index 000000000000..7e157c00e9af
--- /dev/null
+++ b/packages/appstore/src/components/eu-disclaimer/eu-disclaimer.scss
@@ -0,0 +1,24 @@
+.disclaimer {
+ position: fixed;
+ bottom: 3.6rem;
+ width: 100%;
+ min-height: 5rem;
+ z-index: 3;
+ display: flex;
+ align-items: center;
+ backface-visibility: hidden;
+ background: var(--icon-grey-background);
+
+ @include mobile {
+ min-height: 8rem;
+ border: 1px solid var(--icon-grey-background);
+ }
+
+ &-text {
+ padding: 0 3rem;
+
+ @include mobile {
+ padding: 0 1.5rem;
+ }
+ }
+}
diff --git a/packages/appstore/src/components/eu-disclaimer/eu-disclaimer.tsx b/packages/appstore/src/components/eu-disclaimer/eu-disclaimer.tsx
new file mode 100644
index 000000000000..986433698f25
--- /dev/null
+++ b/packages/appstore/src/components/eu-disclaimer/eu-disclaimer.tsx
@@ -0,0 +1,52 @@
+import React from 'react';
+import { Text } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+import { useStore, observer } from '@deriv/stores';
+import './eu-disclaimer.scss';
+
+type TEUDisclaimerProps = {
+ is_wallet?: boolean;
+ wrapperClassName?: string;
+ textClassName?: string;
+};
+type TDisclaimerLocalizedText = {
+ is_wallet?: boolean;
+};
+
+const DisclaimerLocalizedText = ({ is_wallet }: TDisclaimerLocalizedText) =>
+ is_wallet ? (
+ 73% of retail investor accounts lose money when trading CFDs with this provider0>. You should consider whether you understand how CFDs work and whether you can afford to take the high risk of losing your money.'
+ }
+ components={[]}
+ />
+ ) : (
+ EU statutory disclaimer0>: CFDs are complex instruments and come with a high risk of losing money rapidly due to leverage. <0>70.1% of retail investor accounts lose money when trading CFDs with this provider0>. You should consider whether you understand how CFDs work and whether you can afford to take the high risk of losing your money.'
+ }
+ components={[]}
+ />
+ );
+
+const EUDisclaimer = observer(({ is_wallet, wrapperClassName, textClassName }: TEUDisclaimerProps) => {
+ const {
+ ui: { is_mobile },
+ } = useStore();
+
+ return (
+
+
+
+
+
+ );
+});
+
+export default EUDisclaimer;
diff --git a/packages/appstore/src/components/eu-disclaimer/index.ts b/packages/appstore/src/components/eu-disclaimer/index.ts
new file mode 100644
index 000000000000..26dd46512928
--- /dev/null
+++ b/packages/appstore/src/components/eu-disclaimer/index.ts
@@ -0,0 +1,3 @@
+import EUDisclaimer from './eu-disclaimer';
+
+export default EUDisclaimer;
diff --git a/packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.scss b/packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.scss
new file mode 100644
index 000000000000..9f7f0661e52f
--- /dev/null
+++ b/packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.scss
@@ -0,0 +1,24 @@
+.get-more-wallet-card {
+ align-items: center;
+ background: var(--general-section-1);
+ border-radius: 1.6rem;
+ display: flex;
+ height: 16.8rem;
+ justify-content: center;
+ width: 28rem;
+
+ &--button {
+ background: #0796e0; // TODO: Update once dashboard colors are merged
+ border-radius: 1.6rem;
+ height: 3.2rem !important;
+ margin-bottom: 0.8rem;
+ padding: 0.8rem;
+ width: 3.2rem;
+ }
+
+ &--column {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ }
+}
diff --git a/packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.tsx b/packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.tsx
new file mode 100644
index 000000000000..6b3bd26b13d5
--- /dev/null
+++ b/packages/appstore/src/components/get-more-wallet-card/get-more-wallet-card.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Button, Icon, Text } from '@deriv/components';
+import { Localize } from '@deriv/translations';
+
+const GetMoreWalletCard: React.FC = () => {
+ return (
+
+ );
+};
+
+export default GetMoreWalletCard;
diff --git a/packages/appstore/src/components/get-more-wallet-card/index.ts b/packages/appstore/src/components/get-more-wallet-card/index.ts
new file mode 100644
index 000000000000..9c104dfe5983
--- /dev/null
+++ b/packages/appstore/src/components/get-more-wallet-card/index.ts
@@ -0,0 +1,4 @@
+import GetMoreWalletCard from './get-more-wallet-card';
+import './get-more-wallet-card.scss';
+
+export default GetMoreWalletCard;
diff --git a/packages/appstore/src/components/main-title-bar/__tests__/index.spec.tsx b/packages/appstore/src/components/main-title-bar/__tests__/index.spec.tsx
index bf3e21c28bc3..cc72e37e404c 100644
--- a/packages/appstore/src/components/main-title-bar/__tests__/index.spec.tsx
+++ b/packages/appstore/src/components/main-title-bar/__tests__/index.spec.tsx
@@ -1,10 +1,56 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { StoreProvider, mockStore, ExchangeRatesProvider } from '@deriv/stores';
+import { APIProvider } from '@deriv/api';
import MainTitleBar from '..';
jest.mock('Components/wallets-banner', () => jest.fn(() => 'WalletsBanner'));
+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,
+ },
+ ],
+ },
+ },
+ };
+ } else if (name === 'balance') {
+ return {
+ data: {
+ balance: {
+ accounts: {
+ CRW909900: {
+ balance: 1000,
+ },
+ },
+ },
+ },
+ };
+ } else if (name === 'website_status') {
+ return {
+ data: {
+ website_status: {
+ currencies_config: {
+ USD: { type: 'fiat' },
+ },
+ },
+ },
+ };
+ }
+
+ return undefined;
+ }),
+}));
+
describe('MainTitleBar', () => {
const mock_store = mockStore({
modules: {
@@ -21,9 +67,11 @@ describe('MainTitleBar', () => {
const render_container = (mock_store_override?: ReturnType) => {
const wrapper = ({ children }: React.PropsWithChildren) => (
-
- {children}
-
+
+
+ {children}
+
+
);
return render(, {
diff --git a/packages/appstore/src/components/main-title-bar/asset-summary.tsx b/packages/appstore/src/components/main-title-bar/asset-summary.tsx
index 9fa4b3092b8a..c5b3e0d22d2a 100644
--- a/packages/appstore/src/components/main-title-bar/asset-summary.tsx
+++ b/packages/appstore/src/components/main-title-bar/asset-summary.tsx
@@ -6,16 +6,11 @@ import BalanceText from 'Components/elements/text/balance-text';
import { observer, useStore } from '@deriv/stores';
import './asset-summary.scss';
import TotalAssetsLoader from 'Components/pre-loader/total-assets-loader';
-import {
- useTotalAccountBalance,
- useCFDAccounts,
- usePlatformAccounts,
- useTotalAssetCurrency,
- useExchangeRate,
-} from '@deriv/hooks';
-import { isRatesLoaded } from '../../helpers';
+import { useTotalAccountBalance, useCFDAccounts, usePlatformAccounts, useExchangeRate } from '@deriv/hooks';
const AssetSummary = observer(() => {
+ const { exchange_rates } = useExchangeRate();
+
const { traders_hub, client, common, modules } = useStore();
const { selected_account_type, is_eu_user, no_CR_account, no_MF_account } = traders_hub;
const { is_logging_in, is_switching, default_currency, is_landing_company_loaded, is_mt5_allowed } = client;
@@ -29,8 +24,6 @@ const AssetSummary = observer(() => {
const platform_real_balance = useTotalAccountBalance(platform_real_accounts);
const cfd_real_balance = useTotalAccountBalance(cfd_real_accounts);
const cfd_demo_balance = useTotalAccountBalance(cfd_demo_accounts);
- const total_assets_real_currency = useTotalAssetCurrency() ?? 'USD';
- const { exchange_rates } = useExchangeRate();
const is_real = selected_account_type === 'real';
@@ -48,9 +41,9 @@ const AssetSummary = observer(() => {
const should_show_loader =
((is_switching || is_logging_in) && (eu_account || cr_account)) ||
!is_landing_company_loaded ||
+ !exchange_rates ||
is_loading ||
- is_transfer_confirm ||
- !isRatesLoaded(is_real, total_assets_real_currency, platform_real_accounts, cfd_real_accounts, exchange_rates);
+ is_transfer_confirm;
if (should_show_loader) {
return (
diff --git a/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx b/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx
index 830917ee6086..53b27f4ca463 100644
--- a/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx
+++ b/packages/appstore/src/components/modals/account-type-modal/account-type-modal.tsx
@@ -8,7 +8,7 @@ import TradingPlatformIconProps from 'Assets/svgs/trading-platform';
import { TModalContent, TAccountCard, TTradingPlatformAvailableAccount } from './types';
import { TIconTypes } from 'Types';
import { CFD_PLATFORMS } from '@deriv/shared';
-import { getDerivedAccount, getFinancialAccount, getSwapFreeAccount } from '../../../helpers';
+import { getDerivedAccount, getFinancialAccount, getSwapFreeAccount } from '../../../helpers/account-helper';
import { useHasSwapFreeAccount } from '@deriv/hooks';
const AccountCard = ({ selectAccountTypeCard, account_type_card, title_and_type, description, icon }: TAccountCard) => {
diff --git a/packages/appstore/src/components/modals/modal-manager.tsx b/packages/appstore/src/components/modals/modal-manager.tsx
index 59ba031bfc1b..aeaf55879767 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,8 +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';
+import RealWalletsUpgrade from './real-wallets-upgrade/real-wallets-upgrade';
import WalletsMigrationFailed from './wallets-migration-failed';
+import WalletModal from './wallet-modal';
import WalletsUpgradeModal from './wallets-upgrade-modal';
type TCurrentList = DetailsOfEachMT5Loginid & {
@@ -28,6 +30,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, is_eu, is_eu_country, is_populating_mt5_account_list, verification_code } = client;
const { platform } = common;
@@ -151,6 +154,7 @@ const ModalManager = () => {
{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
index 4ec6432909be..e9d77292b34e 100644
--- 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
@@ -2,11 +2,14 @@ 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}
+
+ {children}
+
);
return Component;
};
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/routes/routes.tsx b/packages/appstore/src/components/routes/routes.tsx
index 0750017182ea..58a844e118be 100644
--- a/packages/appstore/src/components/routes/routes.tsx
+++ b/packages/appstore/src/components/routes/routes.tsx
@@ -1,31 +1,42 @@
import * as React from 'react';
-import { Loading } from '@deriv/components';
+// import { Loading } from '@deriv/components';
import { useFeatureFlags /*useWalletsList*/ } from '@deriv/hooks';
import { observer } from '@deriv/stores';
-import { localize } from '@deriv/translations';
-import { routes } from '@deriv/shared';
+import { Localize, localize } from '@deriv/translations';
+import Wallets from '@deriv/wallets';
import Onboarding from 'Modules/onboarding';
import TradersHub from 'Modules/traders-hub';
-import { Switch, useHistory } from 'react-router-dom';
+// import { WalletsModule } from 'Modules/wallets';
+import { Switch } from 'react-router-dom';
import RouteWithSubroutes from './route-with-sub-routes.jsx';
const Routes: React.FC = observer(() => {
//TODO: Uncomment once useWalletList hook is optimized for production release.
const { /*is_wallet_enabled,*/ is_next_wallet_enabled } = useFeatureFlags();
- const history = useHistory();
// const { has_wallet, isLoading } = useWalletsList();
// const should_show_wallets = is_wallet_enabled && has_wallet;
- React.useLayoutEffect(() => {
- if (is_next_wallet_enabled) history.push(routes.wallets);
- }, [history, is_next_wallet_enabled]);
+ let content: React.FC = TradersHub;
+ if (is_next_wallet_enabled) {
+ content = Wallets;
+ }
+ // else if (should_show_wallets) {
+ // content = WalletsModule;
+ // }
+ // if (isLoading) return ;
return (
- }>
+
+
+
+ }
+ >
localize("Trader's Hub")}
/>
[0];
+
+export default {
+ title: 'ToggleAccountType',
+} as Meta;
+
+const Template: Story = () => {
+ const [account_type, setAccountType] = React.useState<'Real' | 'Demo'>('Real');
+ return (
+ setAccountType(event.target.value)}
+ />
+ );
+};
+
+export const AddAppTemplate = Template.bind({});
+AddAppTemplate.args = {};
diff --git a/packages/appstore/src/components/toggle-account-type/toggle-account-type.tsx b/packages/appstore/src/components/toggle-account-type/toggle-account-type.tsx
new file mode 100644
index 000000000000..fb5210c8f7cd
--- /dev/null
+++ b/packages/appstore/src/components/toggle-account-type/toggle-account-type.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { ButtonToggle } from '@deriv/components';
+
+type TAccountTypeProps = {
+ accountTypeChange: any;
+ value: string;
+};
+
+const ToggleAccountType = ({ accountTypeChange, value }: TAccountTypeProps) => {
+ const toggle_options = [
+ { text: 'Real Account', value: 'real' },
+ { text: 'Demo Account', value: 'demo' },
+ ];
+
+ return (
+
+ );
+};
+
+export default ToggleAccountType;
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..5e494418e376
--- /dev/null
+++ b/packages/appstore/src/components/wallet-cards-carousel/cards-slider-swiper.tsx
@@ -0,0 +1,91 @@
+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..c42fb486d956
--- /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..f16b91d0044c
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/__tests__/wallet-header.spec.tsx
@@ -0,0 +1,336 @@
+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 '..';
+import { MT5_ACCOUNT_STATUS } from '@deriv/shared';
+import { useMFAccountStatus } from '@deriv/hooks';
+
+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() })),
+ useMFAccountStatus: 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: '',
+ };
+
+ beforeEach(() => {
+ (useMFAccountStatus as jest.Mock).mockReturnValue(null);
+ });
+
+ 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' });
+ (useMFAccountStatus as jest.Mock).mockReturnValue(MT5_ACCOUNT_STATUS.PENDING);
+
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'MFW1231',
+ },
+ traders_hub: { 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' });
+ (useMFAccountStatus as jest.Mock).mockReturnValue(MT5_ACCOUNT_STATUS.FAILED);
+
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'MFW1231',
+ },
+ traders_hub: { 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' });
+ (useMFAccountStatus as jest.Mock).mockReturnValue(MT5_ACCOUNT_STATUS.NEEDS_VERIFICATION);
+
+ const mocked_props = {
+ ...default_mocked_props,
+ currency: 'EUR',
+ landing_company_name: 'malta',
+ balance: 0,
+ };
+
+ const mocked_store = mockStore({
+ client: {
+ loginid: 'MFW1231',
+ },
+ traders_hub: { 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..ad4bd200e8f7
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header-balance.tsx
@@ -0,0 +1,67 @@
+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';
+import { useMFAccountStatus } from '@deriv/hooks';
+
+type TWalletHeaderBalance = Pick;
+
+const WalletHeaderBalance = observer(({ balance, currency }: TWalletHeaderBalance) => {
+ const {
+ traders_hub: { openFailedVerificationModal, is_eu_user },
+ client,
+ } = useStore();
+ const mf_account_status = useMFAccountStatus();
+
+ const { account_status: { authentication } = {} } = client;
+
+ 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(
+ mf_account_status,
+ openFailedVerificationModal,
+ {
+ platform: '',
+ category: '',
+ type: '',
+ jurisdiction: '',
+ },
+ undefined,
+ { poi_status: authentication?.identity?.status, poa_status: authentication?.document?.status }
+ );
+
+ return (
+
+ {mf_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..ad466d49f9a0
--- /dev/null
+++ b/packages/appstore/src/components/wallet-header/wallet-header.tsx
@@ -0,0 +1,72 @@
+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';
+import { useMFAccountStatus } from '@deriv/hooks';
+
+type TWalletHeader = {
+ wallet_account: TWalletAccount;
+};
+
+const WalletHeader = observer(({ wallet_account }: TWalletHeader) => {
+ const { client } = useStore();
+ const { switchAccount, loginid } = client;
+ const is_active = wallet_account.is_selected;
+ const mf_account_status = useMFAccountStatus();
+
+ 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 () => {
+ if (loginid !== wallet_account.loginid) await switchAccount(wallet_account.loginid);
+ };
+
+ 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/constants/wallet-labels.ts b/packages/appstore/src/constants/wallet-labels.ts
new file mode 100644
index 000000000000..ecc4ac6b0421
--- /dev/null
+++ b/packages/appstore/src/constants/wallet-labels.ts
@@ -0,0 +1,25 @@
+import { localize } from '@deriv/translations';
+
+export const getWalletLabels = (): IWalletLabels => ({
+ DEPOSIT: localize('Deposit'),
+ GET_WALLET: localize('Get more wallets'),
+ RESET: localize('Reset'),
+ SETTINGS: localize('Settings'),
+ TEMPORARILY_UNAVAILABLE: localize('Deposits and withdrawals temporarily unavailable '),
+ TOPUP: localize('Top-up'),
+ TRANSACTIONS: localize('Transactions'),
+ TRANSFER: localize('Transfer'),
+ WITHDRAWAL: localize('Withdrawal'),
+});
+
+interface IWalletLabels {
+ DEPOSIT: string;
+ GET_WALLET: string;
+ RESET: string;
+ SETTINGS: string;
+ TEMPORARILY_UNAVAILABLE: string;
+ TOPUP: string;
+ TRANSACTIONS: string;
+ TRANSFER: string;
+ WITHDRAWAL: string;
+}
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/declarations.d.ts b/packages/appstore/src/declarations.d.ts
index fc184cfa72ea..4a4eef401169 100644
--- a/packages/appstore/src/declarations.d.ts
+++ b/packages/appstore/src/declarations.d.ts
@@ -6,5 +6,6 @@ declare module '*.svg' {
declare module '@deriv/components';
declare module '@deriv/shared';
declare module '@deriv/translations';
+declare module '@deriv/trader';
declare module '@deriv/account';
declare module '@deriv/cfd';
diff --git a/packages/appstore/src/helpers/index.js b/packages/appstore/src/helpers/index.js
new file mode 100644
index 000000000000..2ee0e1ee1c1d
--- /dev/null
+++ b/packages/appstore/src/helpers/index.js
@@ -0,0 +1 @@
+export * from './account-helper';
diff --git a/packages/appstore/src/helpers/index.ts b/packages/appstore/src/helpers/index.ts
deleted file mode 100644
index afd9ee6328ca..000000000000
--- a/packages/appstore/src/helpers/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './account-helper';
-export * from './total-assets-helper';
diff --git a/packages/appstore/src/helpers/total-assets-helper.ts b/packages/appstore/src/helpers/total-assets-helper.ts
deleted file mode 100644
index 5a1c49fb3eb1..000000000000
--- a/packages/appstore/src/helpers/total-assets-helper.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useCFDAccounts, useExchangeRate, usePlatformAccounts } from '@deriv/hooks';
-
-export const isRatesLoaded = (
- is_real: boolean,
- total_assets_real_currency: string,
- platform_real_accounts: ReturnType['real'],
- cfd_real_accounts: ReturnType['real'],
- exchange_rates: ReturnType['exchange_rates']
-) => {
- // for demo
- if (!is_real) return true;
-
- const currencies_need_exchange_rates: string[] = [];
- platform_real_accounts.forEach(account => {
- const target = account.currency || total_assets_real_currency || '';
- if (target && total_assets_real_currency !== target && !currencies_need_exchange_rates.includes(target)) {
- currencies_need_exchange_rates.push(target);
- }
- });
- cfd_real_accounts.forEach(account => {
- const target = account.currency || total_assets_real_currency || '';
- if (target && total_assets_real_currency !== target && !currencies_need_exchange_rates.includes(target)) {
- currencies_need_exchange_rates.push(target);
- }
- });
-
- return (
- currencies_need_exchange_rates.length === 0 ||
- (total_assets_real_currency &&
- exchange_rates?.[total_assets_real_currency] &&
- currencies_need_exchange_rates.length &&
- currencies_need_exchange_rates.length === Object.keys(exchange_rates?.[total_assets_real_currency]).length)
- );
-};
diff --git a/packages/appstore/src/modules/Page404/index.tsx b/packages/appstore/src/modules/Page404/index.tsx
new file mode 100644
index 000000000000..981431ce85c8
--- /dev/null
+++ b/packages/appstore/src/modules/Page404/index.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { PageError } from '@deriv/components';
+import { routes, getUrlBase } from '@deriv/shared';
+
+import { localize } from '@deriv/translations';
+
+const Page404 = () => (
+
+);
+
+export default Page404;
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 105a7c9e647f..dc2f85399f4e 100644
--- a/packages/appstore/src/modules/traders-hub/__tests__/index.spec.tsx
+++ b/packages/appstore/src/modules/traders-hub/__tests__/index.spec.tsx
@@ -1,6 +1,7 @@
import React from 'react';
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'));
@@ -13,7 +14,9 @@ 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/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/core/src/index.tsx b/packages/core/src/index.tsx
index 684189b93c11..f1f69ac55c7b 100644
--- a/packages/core/src/index.tsx
+++ b/packages/core/src/index.tsx
@@ -19,6 +19,12 @@ if (
registerServiceWorker();
}
+// if we don't clear the local storage, then exchange_rates subscription calls won't be made when user refreshes the page
+// check packages/stores/src/providers/ExchangeRatesProvider.tsx
+
+if (window.localStorage.getItem('exchange_rates')) {
+ window.localStorage.removeItem('exchange_rates');
+}
const has_endpoint_url = checkAndSetEndpointFromUrl();
// if has endpoint url, APP will be redirected
diff --git a/packages/hooks/src/__tests__/useExchangeRate.spec.tsx b/packages/hooks/src/__tests__/useExchangeRate.spec.tsx
index 9337a1015710..a8b27a720cc3 100644
--- a/packages/hooks/src/__tests__/useExchangeRate.spec.tsx
+++ b/packages/hooks/src/__tests__/useExchangeRate.spec.tsx
@@ -3,21 +3,16 @@ import { mockStore, StoreProvider, ExchangeRatesProvider } from '@deriv/stores';
import { renderHook } from '@testing-library/react-hooks';
import useExchangeRate from '../useExchangeRate';
-jest.mock('react', () => ({
- ...jest.requireActual('react'),
- useContext: jest.fn(() => ({
- exchange_rates: {
+describe('useExchangeRate', () => {
+ test('should return undefined if currency is not found', async () => {
+ const mockedRates = {
USD: {
EUR: 1.3,
GBP: 1.4,
ETH: 0.0001,
},
- },
- })),
-}));
-
-describe('useExchangeRate', () => {
- test('should return undefined if currency is not found', async () => {
+ };
+ window.localStorage.setItem('exchange_rates', JSON.stringify(mockedRates));
const mock = mockStore({});
const wrapper = ({ children }: { children: JSX.Element }) => (
@@ -31,6 +26,14 @@ describe('useExchangeRate', () => {
});
test('should return correct rate for the given currency other than USD', async () => {
+ const mockedRates = {
+ USD: {
+ EUR: 1.3,
+ GBP: 1.4,
+ ETH: 0.0001,
+ },
+ };
+ window.localStorage.setItem('exchange_rates', JSON.stringify(mockedRates));
const mock = mockStore({});
const wrapper = ({ children }: { children: JSX.Element }) => (
diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts
index ca9a3748a0aa..cbff125d1aec 100644
--- a/packages/hooks/src/index.ts
+++ b/packages/hooks/src/index.ts
@@ -78,4 +78,3 @@ export { default as useWalletTransfer } from './useWalletTransfer';
export { default as useWalletsList } from './useWalletsList';
export { default as useGrowthbookFeatureFlag } from './useGrowthbookFeatureFlag';
export { default as useResidenceSelfDeclaration } from './useResidenceSelfDeclaration';
-export { default as useTotalAssetCurrency } from './useTotalAssetCurrency';
diff --git a/packages/hooks/src/useTotalAccountBalance.ts b/packages/hooks/src/useTotalAccountBalance.ts
index f0954536dd5d..5d805c9c9cc6 100644
--- a/packages/hooks/src/useTotalAccountBalance.ts
+++ b/packages/hooks/src/useTotalAccountBalance.ts
@@ -14,7 +14,7 @@ type TUseTotalAccountBalance = {
const useTotalAccountBalance = (accounts: TUseTotalAccountBalance[]) => {
const total_assets_real_currency = useRealTotalAssetCurrency();
- const { handleSubscription, getExchangeRate } = useExchangeRate();
+ const { handleSubscription, exchange_rates } = useExchangeRate();
if (!accounts.length) return { balance: 0, currency: total_assets_real_currency };
@@ -22,9 +22,11 @@ const useTotalAccountBalance = (accounts: TUseTotalAccountBalance[]) => {
const new_base = account?.account_type === 'demo' ? 'USD' : total_assets_real_currency || '';
const new_target = account.currency || total_assets_real_currency || '';
- if (new_base !== new_target) handleSubscription(new_base, new_target);
+ let new_rate = 1;
+ if (new_base === '' || new_target === '') new_rate = 1;
+ else if (new_base !== new_target) handleSubscription(new_base, new_target);
- const new_rate = getExchangeRate(new_base, new_target);
+ if (exchange_rates && exchange_rates[new_base]) new_rate = exchange_rates[new_base][new_target] || 1;
return total + (account.balance || 0) / new_rate;
}, 0);
diff --git a/packages/shared/package.json b/packages/shared/package.json
index fc25106b2f01..1f51c578b8e8 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -37,7 +37,6 @@
"@testing-library/react-hooks": "^7.0.2",
"@testing-library/user-event": "^13.5.0",
"@types/jsdom": "^20.0.0",
- "@types/object.fromentries": "^2.0.4",
"@types/react": "^18.0.7",
"@types/react-dom": "^18.0.0",
"jsdom": "^21.1.1",
diff --git a/packages/stores/package.json b/packages/stores/package.json
index 2a4d7ca20166..c3804a0c74cc 100644
--- a/packages/stores/package.json
+++ b/packages/stores/package.json
@@ -5,12 +5,12 @@
"main": "src/index.ts",
"dependencies": {
"@deriv/api": "1.0.0",
- "@deriv/shared": "^1.0.0",
"lodash.merge": "^4.6.2",
"mobx": "^6.6.1",
"mobx-persist-store": "1.1.2",
"mobx-react-lite": "^3.4.0",
- "react": "^17.0.2"
+ "react": "^17.0.2",
+ "usehooks-ts": "^2.7.0"
},
"devDependencies": {
"@deriv/api-types": "^1.0.172",
diff --git a/packages/stores/src/providers/ExchangeRatesProvider.tsx b/packages/stores/src/providers/ExchangeRatesProvider.tsx
index 579d12ea23a8..d5615c39f6cc 100644
--- a/packages/stores/src/providers/ExchangeRatesProvider.tsx
+++ b/packages/stores/src/providers/ExchangeRatesProvider.tsx
@@ -1,150 +1,52 @@
-import React from 'react';
-import { ExchangeRatesResponse } from '@deriv/api-types';
-import { useWS } from '@deriv/shared';
+import React, { ReactNode } from 'react';
+import { useLocalStorage } from 'usehooks-ts';
+import { useSubscription } from '@deriv/api';
import ExchangeRatesContext from '../stores/ExchangeRatesContext';
-// Implement this type to prevent install @deriv/deriv-api to this package
-type DerivAPIBasicSubscribe = {
- unsubscribe: () => void;
-};
-type TUnsubscribeFunction = (id: string) => void;
-type TRate = Record>;
-type TPayload = {
- base_currency: string;
- target_currency: string;
-};
type TExchangeRatesProvider = {
- children: React.ReactNode;
+ children: ReactNode;
};
-const ExchangeRatesProvider = ({ children }: TExchangeRatesProvider) => {
- const WS = useWS();
- const [exchangeRates, setExchangeRates] = React.useState({});
- const isMounted = React.useRef(false);
- const subscriptions = React.useRef>();
- const subscriptionsId = React.useRef>();
- const exchangeRatesSubscriptions = React.useRef([]);
-
- const subscribeToCurrencyRate = React.useCallback(
- async (payload: TPayload) => {
- const id = JSON.stringify(payload);
- const matchingSubscription = subscriptions.current?.[id];
- if (matchingSubscription) return { id, subscription: matchingSubscription };
-
- const subscription = WS?.subscribe({
- exchange_rates: 1,
- subscribe: 1,
- ...(payload ?? {}),
- });
+type TRate = Record>;
- subscriptions.current = { ...(subscriptions.current ?? {}), ...{ [id]: subscription } };
- return { id, subscription };
- },
- [WS]
- );
+const ExchangeRatesProvider = ({ children }: TExchangeRatesProvider) => {
+ const [exchange_rates, setExchangeRates] = useLocalStorage('exchange_rates', {});
- const unsubscribeFromCurrencyById: TUnsubscribeFunction = React.useCallback(
- id => {
- const matchingSubscription = subscriptions.current?.[id];
- if (matchingSubscription) WS?.forget(subscriptionsId.current?.[id]);
- },
- [WS]
- );
+ const { subscribe, data, ...rest } = useSubscription('exchange_rates');
- const handleSubscription = React.useCallback(
- async (base_currency: string, target_currency: string) => {
- if (base_currency === '' || target_currency === '' || base_currency === target_currency) return;
- if (exchangeRates[base_currency]?.[target_currency]) return;
+ const handleSubscription = (base_currency: string, target_currency: string) => {
+ if (base_currency === '' || target_currency === '' || base_currency === target_currency) return;
+ if (exchange_rates[base_currency]?.[target_currency]) return;
- const { id, subscription } = await subscribeToCurrencyRate({
+ subscribe({
+ payload: {
base_currency,
target_currency,
- });
-
- if (!exchangeRatesSubscriptions.current.includes(id)) {
- exchangeRatesSubscriptions.current.push(id);
- subscription.subscribe((response: ExchangeRatesResponse) => {
- const rates = response.exchange_rates?.rates;
- const subscriptionId = String(response.subscription?.id);
-
- if (rates && isMounted.current) {
- subscriptionsId.current = { ...(subscriptionsId.current ?? {}), ...{ [id]: subscriptionId } };
-
- setExchangeRates(prev => {
- const currentData = { ...(prev ?? {}) };
- if (currentData) {
- currentData[base_currency] = { ...currentData[base_currency], ...rates };
- return currentData;
- }
- return { [base_currency]: rates };
- });
- }
- });
- }
- },
- [exchangeRates, subscribeToCurrencyRate]
- );
-
- const unsubscribe = React.useCallback(
- (payload: TPayload) => {
- if (payload) {
- const id = JSON.stringify(payload);
- exchangeRatesSubscriptions.current = exchangeRatesSubscriptions.current.filter(s => s !== id);
- unsubscribeFromCurrencyById(id);
- if (isMounted.current)
- setExchangeRates(prev => {
- const currData = { ...(prev ?? {}) };
- delete currData[payload.base_currency];
- return currData;
- });
- }
- },
- [unsubscribeFromCurrencyById]
- );
-
- const unsubscribeAll = React.useCallback(() => {
- exchangeRatesSubscriptions.current.forEach(s => unsubscribeFromCurrencyById(s));
- }, [unsubscribeFromCurrencyById]);
-
- const getExchangeRate = React.useCallback(
- (base: string, target: string) => {
- if (exchangeRates) {
- return exchangeRates?.[base]?.[target] ?? 1;
- }
- return 1;
- },
- [exchangeRates]
- );
+ },
+ });
+ };
React.useEffect(() => {
- isMounted.current = true;
-
- return () => {
- isMounted.current = false;
- const currentSubscriptions = subscriptions.current;
- const currentSubscriptionsIds = subscriptionsId?.current;
-
- if (currentSubscriptions && currentSubscriptionsIds) {
- Object.keys(currentSubscriptions).forEach(key => {
- WS?.forget(currentSubscriptionsIds?.[key]);
- });
- }
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ if (data) {
+ setExchangeRates(prev_rates => {
+ const base_currency = data?.exchange_rates?.base_currency || 'USD';
+ const new_rates = {
+ ...prev_rates,
+ [base_currency]: {
+ ...prev_rates[base_currency],
+ ...data?.exchange_rates?.rates,
+ },
+ };
+ return new_rates;
+ });
+ }
+ }, [data, setExchangeRates]);
- const value = React.useMemo(
- () => ({
- handleSubscription,
- exchange_rates: exchangeRates,
- getExchangeRate,
- unsubscribe,
- unsubscribeAll,
- }),
- [exchangeRates, getExchangeRate, handleSubscription, unsubscribe, unsubscribeAll]
+ return (
+
+ {children}
+
);
-
- return {children};
};
export default ExchangeRatesProvider;
diff --git a/packages/stores/src/stores/ExchangeRatesContext.tsx b/packages/stores/src/stores/ExchangeRatesContext.tsx
index fac58c35b2fb..c0a5857eef36 100644
--- a/packages/stores/src/stores/ExchangeRatesContext.tsx
+++ b/packages/stores/src/stores/ExchangeRatesContext.tsx
@@ -1,13 +1,13 @@
import { createContext } from 'react';
+import { useSubscription } from '@deriv/api';
+
type TRate = Record>;
type TExchangeRatesContext = {
- exchange_rates: TRate;
- getExchangeRate: (base_currency: string, target_currency: string) => number;
handleSubscription: (base_currency: string, target_currency: string) => void;
- unsubscribe: (payload: { base_currency: string; target_currency: string }) => void;
- unsubscribeAll: () => void;
+ exchange_rates: TRate;
+ rest: Omit;
};
const ExchangeRatesContext = createContext(null);