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)} + > +
+
{children}
+
+ {!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 provider. 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 disclaimer: 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 provider. 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} + + +
+ ) : ( +
+
+ +
+ + {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 ( + +
+
{slider}
+
+
+ +
+
+ ); +}); + +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 more' + } + 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 }) => ( +
+
+
0, + })} + > + + {({ field }: FieldProps) => ( + el.type === 'error')} + initial_value={field.value} + label={localize('Amount you send')} + onChange={(value: number) => { + setValues({ + from_amount: value, + to_amount: is_amount_to_input_disabled ? 0 : value, + }); + }} + /> + )} + + + onSelectFromAccount(account, resetForm)} + placeholder={localize('Select a trading account or a Wallet')} + portal_id={portal_id} + setIsWalletNameVisible={setIsWalletNameVisible} + transfer_accounts={transfer_accounts} + wallet_name={active_wallet_name} + value={from_account} + /> +
+ +
+ + {({ field }: FieldProps) => ( + el.type === 'error')} + initial_value={field.value} + label={localize('Amount you receive')} + onChange={(value: number) => { + setValues({ from_amount: value, to_amount: value }); + }} + /> + )} + + + onSelectToAccount(account, resetForm)} + placeholder={!to_account ? localize('Select a trading account or a Wallet') : ''} + portal_id={portal_id} + setIsWalletNameVisible={setIsWalletNameVisible} + transfer_accounts={to_account_list} + transfer_hint={transfer_to_hint} + wallet_name={active_wallet_name} + value={to_account} + /> +
+
+
+ +
+
+ )} +
+
+ ); +}); + +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 ( + +
+ {is_mobile ? : } + +
+ +
+ ); +}); + +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);