From ef66dd9eeac079ee903b5f8fb737f4ecc052b19a Mon Sep 17 00:00:00 2001 From: Farzin Mirzaie <72082844+farzin-fs@users.noreply.github.com> Date: Wed, 19 Oct 2022 13:07:54 +0330 Subject: [PATCH] Farzin/77435/add `useSubscription` hook (#6621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(cashier): :bug: fix `tsconfig` * feat(shared): :sparkles: expose `WS` object from `shared` package via `useWS` * feat(cashier): :sparkles: add `useCountdown` hook * feat(cashier): :sparkles: add `useWS` hook * feat(cashier): :sparkles: add `useStore` hook and `StoreContext` * feat(cashier): :sparkles: add `useVerifyEmail` hook * test(cashier): :white_check_mark: add test for `useCountdown` hook * test(cashier): :white_check_mark: add test for `useWS` hook * fix(cashier): :memo: resolve PR comments * fix(cashier): :memo: resolve PR comments * fix(cashier): :memo: resolve PR comments * fix(cashier): :memo: resolve PR comments * refactor(cashier): :recycle: improve types for `useWS` hook * feat(cashier): :sparkles: add `useSubscription` hook * test(cashier): ✅ add test for `useSubscription` hook * feat(cashier): ✨ add `useNeedAuthentication` hook * feat(cashier): ✨ add `useNeedFinancialAssessment` hook * feat(cashier): ✨ add `useRealSTPAccount` hook * feat(cashier): ✨ add `useNeedTNC` hook * feat(cashier): ✨ add `useDepositLocked` hook * fix(cashier): :memo: resolve PR comments * fix(cashier): :memo: resolve PR comments --- ...untdown.test.tsx => useCountdown.spec.tsx} | 0 .../hooks/__tests__/useDepositLocked.spec.tsx | 153 +++++++++++++++++ .../__tests__/useNeedAuthentication.spec.tsx | 82 ++++++++++ .../useNeedFinancialAssessment.spec.tsx | 154 ++++++++++++++++++ .../src/hooks/__tests__/useNeedTNC.spec.tsx | 150 +++++++++++++++++ .../__tests__/useRealSTPAccount.spec.tsx | 66 ++++++++ .../src/hooks/__tests__/useStore.spec.tsx | 42 +++++ .../hooks/__tests__/useSubscription.spec.tsx | 85 ++++++++++ .../{useWS.test.tsx => useWS.spec.tsx} | 49 +++++- packages/cashier/src/hooks/index.ts | 6 + .../cashier/src/hooks/useDepositLocked.ts | 17 ++ .../src/hooks/useNeedAuthentication.ts | 11 ++ .../src/hooks/useNeedFinancialAssessment.ts | 12 ++ packages/cashier/src/hooks/useNeedTNC.ts | 13 ++ .../cashier/src/hooks/useRealSTPAccount.ts | 13 ++ packages/cashier/src/hooks/useSubscription.ts | 44 +++++ packages/cashier/src/hooks/useWS.ts | 2 +- packages/cashier/src/types/types.ts | 63 ++++++- 18 files changed, 955 insertions(+), 7 deletions(-) rename packages/cashier/src/hooks/__tests__/{useCountdown.test.tsx => useCountdown.spec.tsx} (100%) create mode 100644 packages/cashier/src/hooks/__tests__/useDepositLocked.spec.tsx create mode 100644 packages/cashier/src/hooks/__tests__/useNeedAuthentication.spec.tsx create mode 100644 packages/cashier/src/hooks/__tests__/useNeedFinancialAssessment.spec.tsx create mode 100644 packages/cashier/src/hooks/__tests__/useNeedTNC.spec.tsx create mode 100644 packages/cashier/src/hooks/__tests__/useRealSTPAccount.spec.tsx create mode 100644 packages/cashier/src/hooks/__tests__/useStore.spec.tsx create mode 100644 packages/cashier/src/hooks/__tests__/useSubscription.spec.tsx rename packages/cashier/src/hooks/__tests__/{useWS.test.tsx => useWS.spec.tsx} (64%) create mode 100644 packages/cashier/src/hooks/useDepositLocked.ts create mode 100644 packages/cashier/src/hooks/useNeedAuthentication.ts create mode 100644 packages/cashier/src/hooks/useNeedFinancialAssessment.ts create mode 100644 packages/cashier/src/hooks/useNeedTNC.ts create mode 100644 packages/cashier/src/hooks/useRealSTPAccount.ts create mode 100644 packages/cashier/src/hooks/useSubscription.ts diff --git a/packages/cashier/src/hooks/__tests__/useCountdown.test.tsx b/packages/cashier/src/hooks/__tests__/useCountdown.spec.tsx similarity index 100% rename from packages/cashier/src/hooks/__tests__/useCountdown.test.tsx rename to packages/cashier/src/hooks/__tests__/useCountdown.spec.tsx diff --git a/packages/cashier/src/hooks/__tests__/useDepositLocked.spec.tsx b/packages/cashier/src/hooks/__tests__/useDepositLocked.spec.tsx new file mode 100644 index 000000000000..df4c71dafb32 --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useDepositLocked.spec.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen } from '@testing-library/react'; +import { StoreProvider } from '../useStore'; +import useDepositLocked from '../useDepositLocked'; +import { TRootStore, DeepPartial } from '../../types'; + +const UseDepositLockedExample = () => { + const is_deposit_locked = useDepositLocked(); + + return ( + <> +

{is_deposit_locked ? 'true' : 'false'}

+ + ); +}; + +describe('useDepositLocked', () => { + test('should be false if none of the conditions are met', () => { + const mockRootStore: DeepPartial = { + client: { + is_deposit_lock: false, + is_authentication_needed: false, + is_tnc_needed: false, + is_eu: false, + is_financial_account: false, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: false, + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_deposit_locked = screen.getByTestId('dt_is_deposit_locked'); + expect(is_deposit_locked).toHaveTextContent('false'); + }); + + test('should be true if is_deposit_lock is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_deposit_lock: true, + is_authentication_needed: false, + is_tnc_needed: false, + is_eu: false, + is_financial_account: false, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: false, + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_deposit_locked = screen.getByTestId('dt_is_deposit_locked'); + expect(is_deposit_locked).toHaveTextContent('true'); + }); + + test('should be true if is_need_tnc is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_deposit_lock: false, + is_authentication_needed: false, + is_tnc_needed: true, + is_eu: true, + is_financial_account: false, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: false, + mt5_login_list: [ + { + account_type: 'real', + sub_account_type: 'financial_stp', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_deposit_locked = screen.getByTestId('dt_is_deposit_locked'); + expect(is_deposit_locked).toHaveTextContent('true'); + }); + + test('should be true if is_need_financial_assessment is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_deposit_lock: false, + is_authentication_needed: false, + is_tnc_needed: false, + is_eu: false, + is_financial_account: true, + is_financial_information_incomplete: true, + is_trading_experience_incomplete: false, + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_deposit_locked = screen.getByTestId('dt_is_deposit_locked'); + expect(is_deposit_locked).toHaveTextContent('true'); + }); + + test('should be true if is_need_authentication is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_deposit_lock: false, + is_authentication_needed: true, + is_tnc_needed: false, + is_eu: true, + is_financial_account: false, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: false, + mt5_login_list: [ + { + account_type: 'real', + sub_account_type: 'financial_stp', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_deposit_locked = screen.getByTestId('dt_is_deposit_locked'); + expect(is_deposit_locked).toHaveTextContent('true'); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useNeedAuthentication.spec.tsx b/packages/cashier/src/hooks/__tests__/useNeedAuthentication.spec.tsx new file mode 100644 index 000000000000..72395f610b3e --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useNeedAuthentication.spec.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen } from '@testing-library/react'; +import { StoreProvider } from '../useStore'; +import useNeedAuthentication from '../useNeedAuthentication'; +import { TRootStore, DeepPartial } from '../../types'; + +const UseNeedAuthenticationExample = () => { + const is_need_authentication = useNeedAuthentication(); + + return ( + <> +

{is_need_authentication ? 'true' : 'false'}

+ + ); +}; + +describe('useNeedAuthentication', () => { + test('should be false if is_authentication_needed and is_eu both are false', async () => { + const mockRootStore: DeepPartial = { + client: { + is_authentication_needed: false, + is_eu: false, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_authentication = screen.getByTestId('dt_is_need_authentication'); + expect(is_need_authentication).toHaveTextContent('false'); + }); + + test('should be false if is_authentication_needed is false and is_eu is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_authentication_needed: false, + is_eu: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_authentication = screen.getByTestId('dt_is_need_authentication'); + expect(is_need_authentication).toHaveTextContent('false'); + }); + + test('should be false if is_authentication_needed is true and is_eu is false', async () => { + const mockRootStore: DeepPartial = { + client: { + is_authentication_needed: true, + is_eu: false, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_authentication = screen.getByTestId('dt_is_need_authentication'); + expect(is_need_authentication).toHaveTextContent('false'); + }); + + test('should be true if is_authentication_needed and is_eu both are true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_authentication_needed: true, + is_eu: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_authentication = screen.getByTestId('dt_is_need_authentication'); + expect(is_need_authentication).toHaveTextContent('true'); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useNeedFinancialAssessment.spec.tsx b/packages/cashier/src/hooks/__tests__/useNeedFinancialAssessment.spec.tsx new file mode 100644 index 000000000000..1b1ad3b46f5d --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useNeedFinancialAssessment.spec.tsx @@ -0,0 +1,154 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen } from '@testing-library/react'; +import { StoreProvider } from '../useStore'; +import useNeedFinancialAssessment from '../useNeedFinancialAssessment'; +import { TRootStore, DeepPartial } from '../../types'; + +const UseNeedFinancialAssessmentExample = () => { + const is_need_financial_assessment = useNeedFinancialAssessment(); + + return ( + <> +

{is_need_financial_assessment ? 'true' : 'false'}

+ + ); +}; + +describe('useNeedFinancialAssessment', () => { + test('should be false if is_financial_account, is_financial_information_incomplete and is_trading_experience_incomplete all are false', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: false, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: false, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('false'); + }); + + test('should be false if is_financial_account and is_trading_experience_incomplete are false and is_financial_information_incomplete is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: false, + is_financial_information_incomplete: true, + is_trading_experience_incomplete: false, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('false'); + }); + + test('should be false if is_financial_account and is_financial_information_incomplete are false and is_trading_experience_incomplete is true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: false, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('false'); + }); + + test('should be false if is_financial_account is false but is_financial_information_incomplete and is_trading_experience_incomplete both are true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: false, + is_financial_information_incomplete: true, + is_trading_experience_incomplete: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('false'); + }); + + test('should be false if is_financial_account is true but is_financial_information_incomplete and is_trading_experience_incomplete both are false', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: true, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: false, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('false'); + }); + + test('should be true if is_financial_account and is_financial_information_incomplete are true and is_trading_experience_incomplete is false', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: true, + is_financial_information_incomplete: true, + is_trading_experience_incomplete: false, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('true'); + }); + + test('should be true if is_financial_account and is_trading_experience_incomplete are true and is_financial_information_incomplete is false', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: true, + is_financial_information_incomplete: false, + is_trading_experience_incomplete: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('true'); + }); + + test('should be true if is_financial_account, is_financial_information_incomplete and is_trading_experience_incomplete all are true', async () => { + const mockRootStore: DeepPartial = { + client: { + is_financial_account: true, + is_financial_information_incomplete: true, + is_trading_experience_incomplete: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_financial_assessment = screen.getByTestId('dt_is_need_financial_assessment'); + expect(is_need_financial_assessment).toHaveTextContent('true'); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useNeedTNC.spec.tsx b/packages/cashier/src/hooks/__tests__/useNeedTNC.spec.tsx new file mode 100644 index 000000000000..d2bf7d6f1a2c --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useNeedTNC.spec.tsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen } from '@testing-library/react'; +import { StoreProvider } from '../useStore'; +import useNeedTNC from '../useNeedTNC'; +import { TRootStore, DeepPartial } from '../../types'; + +const UseNeedTNCExample = () => { + const is_need_tnc = useNeedTNC(); + + return ( + <> +

{is_need_tnc ? 'true' : 'false'}

+ + ); +}; + +describe('useNeedTNC', () => { + test('should be false if is_tnc_needed and is_eu are false and does not have an real STP account', async () => { + const mockRootStore: DeepPartial = { + client: { + is_tnc_needed: false, + is_eu: false, + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_tnc = screen.getByTestId('dt_is_need_tnc'); + expect(is_need_tnc).toHaveTextContent('false'); + }); + + test('should be false if is_tnc_needed is false but is_eu is true and does not have an real STP account', async () => { + const mockRootStore: DeepPartial = { + client: { + is_tnc_needed: false, + is_eu: true, + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_tnc = screen.getByTestId('dt_is_need_tnc'); + expect(is_need_tnc).toHaveTextContent('false'); + }); + + test('should be false if is_tnc_needed and is_eu are false but has an real STP account', async () => { + const mockRootStore: DeepPartial = { + client: { + is_tnc_needed: false, + is_eu: true, + mt5_login_list: [ + { + account_type: 'real', + sub_account_type: 'financial_stp', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_tnc = screen.getByTestId('dt_is_need_tnc'); + expect(is_need_tnc).toHaveTextContent('false'); + }); + + test('should be true if is_tnc_needed is true and is_eu is false but has an real STP account', async () => { + const mockRootStore: DeepPartial = { + client: { + is_tnc_needed: true, + is_eu: false, + mt5_login_list: [ + { + account_type: 'real', + sub_account_type: 'financial_stp', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_tnc = screen.getByTestId('dt_is_need_tnc'); + expect(is_need_tnc).toHaveTextContent('true'); + }); + + test('should be true if is_tnc_needed and is_eu are true and does not have an real STP account', async () => { + const mockRootStore: DeepPartial = { + client: { + is_tnc_needed: true, + is_eu: true, + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_tnc = screen.getByTestId('dt_is_need_tnc'); + expect(is_need_tnc).toHaveTextContent('true'); + }); + + test('should be true if is_tnc_needed and is_eu are true and has an real STP account', async () => { + const mockRootStore: DeepPartial = { + client: { + is_tnc_needed: true, + is_eu: true, + mt5_login_list: [ + { + account_type: 'real', + sub_account_type: 'financial_stp', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const is_need_tnc = screen.getByTestId('dt_is_need_tnc'); + expect(is_need_tnc).toHaveTextContent('true'); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useRealSTPAccount.spec.tsx b/packages/cashier/src/hooks/__tests__/useRealSTPAccount.spec.tsx new file mode 100644 index 000000000000..5fb155671059 --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useRealSTPAccount.spec.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen } from '@testing-library/react'; +import { StoreProvider } from '../useStore'; +import useRealSTPAccount from '../useRealSTPAccount'; +import { TRootStore, DeepPartial } from '../../types'; + +const UseRealSTPAccountExample = () => { + const has_real_stp_account = useRealSTPAccount(); + + return ( + <> +

{has_real_stp_account ? 'true' : 'false'}

+ + ); +}; + +describe('useRealSTPAccount', () => { + test('should be false if does not have an account type of real with sub account type of financial_stp', async () => { + const mockRootStore: DeepPartial = { + client: { + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial_stp', + }, + { + account_type: 'real', + sub_account_type: 'financial', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const has_real_stp_account = screen.getByTestId('dt_has_real_stp_account'); + expect(has_real_stp_account).toHaveTextContent('false'); + }); + + test('should be true if has an account type of real with sub account type of financial_stp', async () => { + const mockRootStore: DeepPartial = { + client: { + mt5_login_list: [ + { + account_type: 'demo', + sub_account_type: 'financial', + }, + { + account_type: 'real', + sub_account_type: 'financial_stp', + }, + ], + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const has_real_stp_account = screen.getByTestId('dt_has_real_stp_account'); + expect(has_real_stp_account).toHaveTextContent('true'); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useStore.spec.tsx b/packages/cashier/src/hooks/__tests__/useStore.spec.tsx new file mode 100644 index 000000000000..4d3619c04270 --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useStore.spec.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen } from '@testing-library/react'; +import { StoreProvider, useStore } from '../useStore'; +import { TRootStore, DeepPartial } from '../../types'; + +const UseStoreExample = () => { + const store = useStore(); + + return ( + <> +

{store.client.email}

+

{store.ui.is_dark_mode_on ? 'true' : 'false'}

+ + ); +}; + +describe('useStore', () => { + test('should throw an error if StoreContext has not been provided', async () => { + expect(() => render()).toThrowError('useStore must be used within StoreContext'); + }); + + test('should be able to access store data if StoreContext has been provided', async () => { + const mockRootStore: DeepPartial = { + client: { + email: 'john@company.com', + }, + ui: { + is_dark_mode_on: true, + }, + }; + + render(, { + wrapper: ({ children }) => {children}, + }); + + const email = screen.getByTestId('dt_email'); + const is_dark_mode_on = screen.getByTestId('dt_is_dark_mode_on'); + expect(email).toHaveTextContent('john@company.com'); + expect(is_dark_mode_on).toHaveTextContent('true'); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useSubscription.spec.tsx b/packages/cashier/src/hooks/__tests__/useSubscription.spec.tsx new file mode 100644 index 000000000000..53b6636f0094 --- /dev/null +++ b/packages/cashier/src/hooks/__tests__/useSubscription.spec.tsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +// Todo: After upgrading to react 18 we should use @testing-library/react-hooks instead. +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { useWS as useWSShared } from '@deriv/shared'; +import useSubscription from '../useSubscription'; +import { TSocketRequestProps, TSocketSubscribableEndpointNames } from '../../types'; + +jest.mock('@deriv/shared'); + +const mockUseWSShared = useWSShared as jest.MockedFunction; + +const UseSubscriptionExample = ({ + name, + request, +}: { + name: T; + request?: TSocketRequestProps; +}) => { + const WS = useSubscription(name); + + return ( + +

{WS.is_loading ? 'true' : 'false'}

+

{WS.error ? JSON.stringify(WS.error) : 'undefined'}

+

{WS.data ? JSON.stringify(WS.data) : 'undefined'}

+ +
+ ); +}; + +describe('useSubscription', () => { + test('should subscribe to p2p_order_info and get the order updates', async () => { + mockUseWSShared.mockReturnValue({ + subscribe: jest.fn(() => { + return { + subscribe: async (onData, onError) => { + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + + await delay(500); + + onData({ p2p_order_info: { status: 'pending' } }); + + await delay(500); + + onData({ p2p_order_info: { status: 'buyer-confirmed' } }); + + await delay(500); + + onData({ p2p_order_info: { status: 'disputed' } }); + + await delay(500); + + onError({ error: { code: 'Foo', message: 'Error message' } }); + + await delay(500); + + onData({ p2p_order_info: { status: 'completed' } }); + + return { unsubscribe: () => Promise.resolve() }; + }, + }; + }), + }); + + render(); + + const is_loading = screen.getByTestId('dt_is_loading'); + const error = screen.getByTestId('dt_error'); + const data = screen.getByTestId('dt_data'); + const subscribe = screen.getByTestId('dt_subscribe'); + + expect(is_loading).toHaveTextContent('false'); + expect(error).toHaveTextContent('undefined'); + expect(data).toHaveTextContent('undefined'); + userEvent.click(subscribe); + await waitFor(() => expect(data).toHaveTextContent('{"status":"pending"}')); + await waitFor(() => expect(data).toHaveTextContent('{"status":"buyer-confirmed"}')); + await waitFor(() => expect(data).toHaveTextContent('{"status":"disputed"}')); + await waitFor(() => expect(error).toHaveTextContent('{"code":"Foo","message":"Error message"}')); + await waitFor(() => expect(data).toHaveTextContent('{"status":"completed"}')); + }); +}); diff --git a/packages/cashier/src/hooks/__tests__/useWS.test.tsx b/packages/cashier/src/hooks/__tests__/useWS.spec.tsx similarity index 64% rename from packages/cashier/src/hooks/__tests__/useWS.test.tsx rename to packages/cashier/src/hooks/__tests__/useWS.spec.tsx index 3f71b69a7a99..fe6ec1d86138 100644 --- a/packages/cashier/src/hooks/__tests__/useWS.test.tsx +++ b/packages/cashier/src/hooks/__tests__/useWS.spec.tsx @@ -4,7 +4,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useWS as useWSShared } from '@deriv/shared'; import useWS from '../useWS'; -import { TSocketEndpointNames, TSocketRequestProps } from '../../types'; +import { TSocketEndpointNames, TSocketRequestProps, TSocketResponse } from '../../types'; jest.mock('@deriv/shared'); @@ -46,7 +46,13 @@ describe('useWS', () => { test('should call ping and get pong in response', async () => { mockUseWSShared.mockReturnValue({ - send: jest.fn(() => Promise.resolve({ ping: 'pong' })), + send: jest.fn(() => + Promise.resolve>({ + msg_type: 'ping', + ping: 'pong', + echo_req: {}, + }) + ), }); render(); @@ -68,7 +74,13 @@ describe('useWS', () => { test('should call verify_email and get 1 in response', async () => { mockUseWSShared.mockReturnValue({ - send: jest.fn(() => Promise.resolve({ verify_email: 1 })), + send: jest.fn(() => + Promise.resolve>({ + verify_email: 1, + msg_type: 'verify_email', + echo_req: {}, + }) + ), }); render( @@ -90,5 +102,34 @@ describe('useWS', () => { await waitFor(() => expect(is_loading).toHaveTextContent('false')); }); - // TODO: Add more test cases. + test('should call cashier and get ASK_TNC_APPROVAL error code in response', async () => { + mockUseWSShared.mockReturnValue({ + send: jest.fn(() => + Promise.resolve>({ + msg_type: 'cashier', + echo_req: {}, + error: { + code: 'ASK_TNC_APPROVAL', + message: 'Error message', + }, + }) + ), + }); + + render(); + + const is_loading = screen.getByTestId('dt_is_loading'); + const error = screen.getByTestId('dt_error'); + const data = screen.getByTestId('dt_data'); + const send = screen.getByTestId('dt_send'); + + expect(is_loading).toHaveTextContent('false'); + expect(error).toHaveTextContent('undefined'); + expect(data).toHaveTextContent('undefined'); + userEvent.click(send); + await waitFor(() => expect(is_loading).toHaveTextContent('true')); + await waitFor(() => expect(data).toHaveTextContent('undefined')); + await waitFor(() => expect(error).toHaveTextContent('{"code":"ASK_TNC_APPROVAL","message":"Error message"}')); + await waitFor(() => expect(is_loading).toHaveTextContent('false')); + }); }); diff --git a/packages/cashier/src/hooks/index.ts b/packages/cashier/src/hooks/index.ts index 4ed6c4b7c354..729085c341e8 100644 --- a/packages/cashier/src/hooks/index.ts +++ b/packages/cashier/src/hooks/index.ts @@ -1,4 +1,10 @@ export * from './useStore'; export { default as useCountdown } from './useCountdown'; export { default as useWS } from './useWS'; +export { default as useSubscription } from './useSubscription'; export { default as useVerifyEmail } from './useVerifyEmail'; +export { default as useNeedAuthentication } from './useNeedAuthentication'; +export { default as useNeedFinancialAssessment } from './useNeedFinancialAssessment'; +export { default as useRealSTPAccount } from './useRealSTPAccount'; +export { default as useNeedTNC } from './useNeedTNC'; +export { default as useDepositLocked } from './useDepositLocked'; diff --git a/packages/cashier/src/hooks/useDepositLocked.ts b/packages/cashier/src/hooks/useDepositLocked.ts new file mode 100644 index 000000000000..75282fc17181 --- /dev/null +++ b/packages/cashier/src/hooks/useDepositLocked.ts @@ -0,0 +1,17 @@ +import useNeedAuthentication from './useNeedAuthentication'; +import useNeedFinancialAssessment from './useNeedFinancialAssessment'; +import useNeedTNC from './useNeedTNC'; +import { useStore } from './useStore'; + +const useDepositLocked = () => { + const { client } = useStore(); + const { is_deposit_lock } = client; + const is_need_authentication = useNeedAuthentication(); + const is_need_tnc = useNeedTNC(); + const is_need_financial_assessment = useNeedFinancialAssessment(); + const is_deposit_locked = is_deposit_lock || is_need_authentication || is_need_tnc || is_need_financial_assessment; + + return is_deposit_locked; +}; + +export default useDepositLocked; diff --git a/packages/cashier/src/hooks/useNeedAuthentication.ts b/packages/cashier/src/hooks/useNeedAuthentication.ts new file mode 100644 index 000000000000..43879816774c --- /dev/null +++ b/packages/cashier/src/hooks/useNeedAuthentication.ts @@ -0,0 +1,11 @@ +import { useStore } from './useStore'; + +const useNeedAuthentication = () => { + const { client } = useStore(); + const { is_authentication_needed, is_eu } = client; + const is_need_authentication = is_authentication_needed && is_eu; + + return is_need_authentication; +}; + +export default useNeedAuthentication; diff --git a/packages/cashier/src/hooks/useNeedFinancialAssessment.ts b/packages/cashier/src/hooks/useNeedFinancialAssessment.ts new file mode 100644 index 000000000000..8b88bde779c5 --- /dev/null +++ b/packages/cashier/src/hooks/useNeedFinancialAssessment.ts @@ -0,0 +1,12 @@ +import { useStore } from './useStore'; + +const useNeedFinancialAssessment = () => { + const { client } = useStore(); + const { is_financial_account, is_financial_information_incomplete, is_trading_experience_incomplete } = client; + const is_need_financial_assessment = + is_financial_account && (is_financial_information_incomplete || is_trading_experience_incomplete); + + return is_need_financial_assessment; +}; + +export default useNeedFinancialAssessment; diff --git a/packages/cashier/src/hooks/useNeedTNC.ts b/packages/cashier/src/hooks/useNeedTNC.ts new file mode 100644 index 000000000000..d1a077d0e68f --- /dev/null +++ b/packages/cashier/src/hooks/useNeedTNC.ts @@ -0,0 +1,13 @@ +import useRealSTPAccount from './useRealSTPAccount'; +import { useStore } from './useStore'; + +const useNeedTNC = () => { + const { client } = useStore(); + const { is_eu, is_tnc_needed } = client; + const has_real_stp_account = useRealSTPAccount(); + const is_need_tnc = (is_eu || has_real_stp_account) && is_tnc_needed; + + return is_need_tnc; +}; + +export default useNeedTNC; diff --git a/packages/cashier/src/hooks/useRealSTPAccount.ts b/packages/cashier/src/hooks/useRealSTPAccount.ts new file mode 100644 index 000000000000..3e8ecf88eeb7 --- /dev/null +++ b/packages/cashier/src/hooks/useRealSTPAccount.ts @@ -0,0 +1,13 @@ +import { useStore } from './useStore'; + +const useRealSTPAccount = () => { + const { client } = useStore(); + const { mt5_login_list } = client; + const has_real_stp_account = mt5_login_list.some( + item => item.account_type === 'real' && item.sub_account_type === 'financial_stp' + ); + + return has_real_stp_account; +}; + +export default useRealSTPAccount; diff --git a/packages/cashier/src/hooks/useSubscription.ts b/packages/cashier/src/hooks/useSubscription.ts new file mode 100644 index 000000000000..ac1340add3a3 --- /dev/null +++ b/packages/cashier/src/hooks/useSubscription.ts @@ -0,0 +1,44 @@ +import { TSocketRequestProps, TSocketResponseData, TSocketSubscribableEndpointNames } from 'Types'; +import { useWS as useWSShared } from '@deriv/shared'; +import { useState } from 'react'; + +const useSubscription = (name: T) => { + const [is_loading, setIsLoading] = useState(false); + const [is_subscribed, setSubscribed] = useState(false); + const [error, setError] = useState(); + const [data, setData] = useState>(); + const [subscriber, setSubscriber] = useState<{ unsubscribe?: VoidFunction }>(); + const WS = useWSShared(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onData = (response: any) => { + setData(response[name]); + setIsLoading(false); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const onError = (response: any) => { + setError(response.error); + setIsLoading(false); + }; + + const subscribe = (...props: TSocketRequestProps extends never ? [undefined?] : [TSocketRequestProps]) => { + setIsLoading(true); + setSubscribed(true); + + try { + setSubscriber(WS.subscribe({ [name]: 1, subscribe: 1, ...(props[0] || {}) }).subscribe(onData, onError)); + } catch (e) { + setError(e); + } + }; + + const unsubscribe = () => { + subscriber?.unsubscribe?.(); + setSubscribed(false); + }; + + return { subscribe, unsubscribe, is_loading, is_subscribed, error, data }; +}; + +export default useSubscription; diff --git a/packages/cashier/src/hooks/useWS.ts b/packages/cashier/src/hooks/useWS.ts index 8a6e36313fe0..39511a999d8d 100644 --- a/packages/cashier/src/hooks/useWS.ts +++ b/packages/cashier/src/hooks/useWS.ts @@ -14,7 +14,7 @@ const useWS = (name: T) => { setIsLoading(true); try { - const response = await WS.send({ [name]: 1, ...props }); + const response = await WS.send({ [name]: 1, ...(props[0] || {}) }); if (response.error) { setError(response.error); diff --git a/packages/cashier/src/types/types.ts b/packages/cashier/src/types/types.ts index 64d39082a20b..5031a516ef7a 100644 --- a/packages/cashier/src/types/types.ts +++ b/packages/cashier/src/types/types.ts @@ -9,6 +9,24 @@ import { P2POrderInformationResponse, ServerStatusRequest, ServerStatusResponse, + TermsAndConditionsApprovalRequest, + TermsAndConditionsApprovalResponse, + CashierInformationRequest, + CashierInformationResponse, + CryptocurrencyConfigurationsRequest, + CryptocurrencyConfigurationsResponse, + PaymentAgentTransferRequest, + PaymentAgentTransferResponse, + PaymentAgentListRequest, + PaymentAgentListResponse, + PaymentAgentDetailsRequest, + PaymentAgentDetailsResponse, + PaymentAgentWithdrawRequest, + PaymentAgentWithdrawResponse, + TransferBetweenAccountsRequest, + TransferBetweenAccountsResponse, + BalanceRequest, + BalanceResponse, } from '@deriv/api-types'; import { KeysMatching } from './utils'; @@ -33,20 +51,61 @@ export type TSocketEndpoints = { request: ServerStatusRequest; response: ServerStatusResponse; }; + tnc_approval: { + request: TermsAndConditionsApprovalRequest; + response: TermsAndConditionsApprovalResponse; + }; + cashier: { + request: CashierInformationRequest; + response: CashierInformationResponse; + }; + crypto_config: { + request: CryptocurrencyConfigurationsRequest; + response: CryptocurrencyConfigurationsResponse; + }; + paymentagent_transfer: { + request: PaymentAgentTransferRequest; + response: PaymentAgentTransferResponse; + }; + paymentagent_list: { + request: PaymentAgentListRequest; + response: PaymentAgentListResponse; + }; + paymentagent_details: { + request: PaymentAgentDetailsRequest; + response: PaymentAgentDetailsResponse; + }; + paymentagent_withdraw: { + request: PaymentAgentWithdrawRequest; + response: PaymentAgentWithdrawResponse; + }; + transfer_between_accounts: { + request: TransferBetweenAccountsRequest; + response: TransferBetweenAccountsResponse; + }; + balance: { + request: BalanceRequest; + response: BalanceResponse; + }; }; export type TSocketEndpointNames = keyof TSocketEndpoints; + export type TSocketSubscribableEndpointNames = | KeysMatching | 'exchange_rates'; + export type TSocketResponse = TSocketEndpoints[T]['response']; + export type TSocketResponseData = TSocketResponse[T]; -export type TSocketResponseSubscriptionInformation = TSocketResponse['subscription']; + export type TSocketRequest = TSocketEndpoints[T]['request']; + type TSocketRequestCleaned = Omit< TSocketRequest, - (T extends KeysMatching, 1> ? T : never) | 'passthrough' | 'req_id' + (T extends KeysMatching, 1> ? T : never) | 'passthrough' | 'req_id' | 'subscribe' >; + export type TSocketRequestProps = TSocketRequestCleaned extends Record ? never : TSocketRequestCleaned;