From faa5efa4626b81921910e3cef53e39587838116a Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Wed, 26 Jul 2023 15:39:04 +0400 Subject: [PATCH 01/22] refactor: refactored api-token index and test files --- .../api-token/_tests_/api-token.spec.js | 136 +++++++++--------- .../Components/api-token/api-token-card.tsx | 14 +- .../Components/api-token/api-token-footer.tsx | 21 --- .../api-token/api-token-overlay.tsx | 31 ---- .../src/Components/api-token/api-token.tsx | 75 ++++------ packages/account/src/Types/context.type.ts | 2 - packages/core/src/Stores/ui-store.js | 6 + packages/stores/types.ts | 1 + 8 files changed, 103 insertions(+), 183 deletions(-) delete mode 100644 packages/account/src/Components/api-token/api-token-footer.tsx delete mode 100644 packages/account/src/Components/api-token/api-token-overlay.tsx diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.js b/packages/account/src/Components/api-token/_tests_/api-token.spec.js index 1243918e1153..fd963869f9af 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.js +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.js @@ -59,8 +59,6 @@ describe('', () => { const learn_more_title = 'Learn more about API token'; const read_scope_description = 'This scope will allow third-party apps to view your account activity, settings, limits, balance sheets, trade purchase history, and more.'; - const our_access_description = - "To access our mobile apps and other third-party apps, you'll first need to generate an API token."; const trading_info_scope_description = 'This scope will allow third-party apps to withdraw to payment agents and make inter-account transfers for you.'; const select_scopes_msg = 'Select scopes based on the access you need.'; @@ -77,12 +75,12 @@ describe('', () => { client: { is_switching: false, }, + ui: { + is_desktop: false, + is_mobile: true, + }, }); const mock_props = { - footer_ref: undefined, - is_app_settings: false, - overlay_ref: undefined, - setIsOverlayShown: jest.fn(), WS: { apiToken: jest.fn(() => Promise.resolve({ @@ -120,7 +118,6 @@ describe('', () => { expect(await screen.findByText(token_using_description)).toBeInTheDocument(); expect(await screen.findByText(trade_scope_description)).toBeInTheDocument(); expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); - expect(await screen.findByText(your_access_description)).toBeInTheDocument(); expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); }); @@ -150,69 +147,68 @@ describe('', () => { expect(screen.queryByText(read_scope_description)).not.toBeInTheDocument(); }); - it('should render ApiToken component without app_settings and footer for mobile', async () => { - isMobile.mockReturnValueOnce(true); - isDesktop.mockReturnValueOnce(false); - - render( - - - - ); - - expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); - expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); - expect(await screen.findByText(trading_info_scope_description)).toBeInTheDocument(); - expect(await screen.findByText(select_scopes_msg)).toBeInTheDocument(); - expect(await screen.findByText(token_creation_description)).toBeInTheDocument(); - expect(await screen.findByText(token_using_description)).toBeInTheDocument(); - expect(await screen.findByText(trade_scope_description)).toBeInTheDocument(); - expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); - expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); - expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); - }); - - it('should render ApiToken component with app_settings', async () => { - mock_props.is_app_settings = true; - - render( - - - - ); - - await waitFor(() => { - expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); - }); - }); - - it('should render ApiTokenFooter, show and close ApiTokenOverlay after triggering links', async () => { - const footer_portal_root_el = document.createElement('div'); - document.body.appendChild(footer_portal_root_el); - const overlay_portal_root_el = document.createElement('div'); - document.body.appendChild(overlay_portal_root_el); - - mock_props.footer_ref = footer_portal_root_el; - mock_props.overlay_ref = overlay_portal_root_el; - - render( - - - - ); - - expect(await screen.findByText(learn_more_title)).toBeInTheDocument(); - expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); - - fireEvent.click(await screen.findByText(learn_more_title)); - expect(await screen.findByText(our_access_description)).toBeInTheDocument(); - - fireEvent.click(await screen.findByRole('button', { name: /done/i })); - expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); - - document.body.removeChild(footer_portal_root_el); - document.body.removeChild(overlay_portal_root_el); - }); + // isMobile.mockReturnValueOnce(true); + // isDesktop.mockReturnValueOnce(false); + + // render( + // + // + // + // ); + + // expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); + // expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); + // expect(await screen.findByText(trading_info_scope_description)).toBeInTheDocument(); + // expect(await screen.findByText(select_scopes_msg)).toBeInTheDocument(); + // expect(await screen.findByText(token_creation_description)).toBeInTheDocument(); + // expect(await screen.findByText(token_using_description)).toBeInTheDocument(); + // expect(await screen.findByText(trade_scope_description)).toBeInTheDocument(); + // expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); + // expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); + // expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); + // }); + + // it('should render ApiToken component with app_settings', async () => { + // mock_props.is_app_settings = true; + + // render( + // + // + // + // ); + + // await waitFor(() => { + // expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); + // }); + // }); + + // it('should render ApiTokenFooter, show and close ApiTokenOverlay after triggering links', async () => { + // const footer_portal_root_el = document.createElement('div'); + // document.body.appendChild(footer_portal_root_el); + // const overlay_portal_root_el = document.createElement('div'); + // document.body.appendChild(overlay_portal_root_el); + + // mock_props.footer_ref = footer_portal_root_el; + // mock_props.overlay_ref = overlay_portal_root_el; + + // render( + // + // + // + // ); + + // expect(await screen.findByText(learn_more_title)).toBeInTheDocument(); + // expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); + + // fireEvent.click(await screen.findByText(learn_more_title)); + // expect(await screen.findByText(our_access_description)).toBeInTheDocument(); + + // fireEvent.click(await screen.findByRole('button', { name: /done/i })); + // expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); + + // document.body.removeChild(footer_portal_root_el); + // document.body.removeChild(overlay_portal_root_el); + // }); it('should choose checkbox, enter a valid value and create token', async () => { render( diff --git a/packages/account/src/Components/api-token/api-token-card.tsx b/packages/account/src/Components/api-token/api-token-card.tsx index f44bb77b93e4..d1bc536ba8d3 100644 --- a/packages/account/src/Components/api-token/api-token-card.tsx +++ b/packages/account/src/Components/api-token/api-token-card.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Field, FieldProps } from 'formik'; +import { Field, FieldProps, useFormikContext } from 'formik'; import { CompositeCheckbox } from '@deriv/components'; type TApiTokenCard = { @@ -7,17 +7,10 @@ type TApiTokenCard = { display_name: string; name: string; value: boolean; - setFieldValue: (name: string, value: boolean) => void; }; -const ApiTokenCard = ({ - name, - value, - display_name, - description, - setFieldValue, - children, -}: React.PropsWithChildren) => { +const ApiTokenCard = ({ name, value, display_name, description, children }: React.PropsWithChildren) => { + const { setFieldValue } = useFormikContext(); return ( {({ field }: FieldProps) => { @@ -27,7 +20,6 @@ const ApiTokenCard = ({ onChange={() => setFieldValue(name, !value)} value={value} className='api-token__checkbox' - defaultChecked={value} label={display_name} description={description} > diff --git a/packages/account/src/Components/api-token/api-token-footer.tsx b/packages/account/src/Components/api-token/api-token-footer.tsx deleted file mode 100644 index c9a4af639006..000000000000 --- a/packages/account/src/Components/api-token/api-token-footer.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import { createPortal } from 'react-dom'; -import { Text } from '@deriv/components'; -import { Localize } from '@deriv/translations'; -import ApiTokenContext from './api-token-context'; -import { TApiContext } from 'Types'; - -const ApiTokenFooter = () => { - const { footer_ref, toggleOverlay } = React.useContext(ApiTokenContext); - - return createPortal( - - - - - , - footer_ref - ); -}; - -export default ApiTokenFooter; diff --git a/packages/account/src/Components/api-token/api-token-overlay.tsx b/packages/account/src/Components/api-token/api-token-overlay.tsx deleted file mode 100644 index b7572884102a..000000000000 --- a/packages/account/src/Components/api-token/api-token-overlay.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import { Popup } from '@deriv/components'; -import { Localize, localize } from '@deriv/translations'; -import ApiTokenContext from './api-token-context'; -import { TApiContext } from 'Types'; - -const ApiTokenOverlay = () => { - const { overlay_ref, toggleOverlay } = React.useContext(ApiTokenContext); - - return ( - - ), - }, - ]} - done_text={localize('Done')} - overlay_ref={overlay_ref} - title={localize('API Token')} - toggleOverlay={toggleOverlay} - /> - ); -}; - -export default ApiTokenOverlay; diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index 558e16a5725c..5bb1d9d509b0 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -1,15 +1,13 @@ import React from 'react'; import classNames from 'classnames'; -import { Formik, Form, Field, FormikValues, FormikErrors, FieldProps } from 'formik'; +import { Formik, Form, Field, FormikErrors, FieldProps, FormikHelpers } from 'formik'; import { Timeline, Input, Button, ThemedScrollbars, Loading } from '@deriv/components'; import InlineNoteWithIcon from '../inline-note-with-icon'; -import { isDesktop, isMobile, getPropertyValue, useIsMounted, WS } from '@deriv/shared'; +import { getPropertyValue, useIsMounted, WS } from '@deriv/shared'; import { localize } from '@deriv/translations'; import LoadErrorMessage from 'Components/load-error-message'; import ApiTokenArticle from './api-token-article'; import ApiTokenCard from './api-token-card'; -import ApiTokenFooter from './api-token-footer'; -import ApiTokenOverlay from './api-token-overlay'; import ApiTokenTable from './api-token-table'; import ApiTokenContext from './api-token-context'; import { TToken } from 'Types'; @@ -30,22 +28,20 @@ type AptTokenState = { is_delete_success: boolean; }; -export type TApiToken = { - footer_ref: Element | DocumentFragment | undefined; - is_app_settings: boolean; - overlay_ref: - | undefined - | ((...args: unknown[]) => unknown) - | import('prop-types').InferProps<{ - current: import('prop-types').Requireable; - }>; - setIsOverlayShown: (is_overlay_shown: boolean | undefined) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any +type TApiTokenForm = { + token_name: string; + read: boolean; + trade: boolean; + payments: boolean; + trading_information: boolean; + admin: boolean; }; -const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown }: TApiToken) => { +const ApiToken = () => { const { client } = useStore(); const { is_switching } = client; + const { ui } = useStore(); + const { is_desktop, is_mobile } = ui; const isMounted = useIsMounted(); const prev_is_switching = React.useRef(is_switching); const [state, setState] = React.useReducer( @@ -81,12 +77,6 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown // eslint-disable-next-line react-hooks/exhaustive-deps }, [is_switching]); - React.useEffect(() => { - if (typeof setIsOverlayShown === 'function') { - setIsOverlayShown(state.is_overlay_shown); - } - }, [state.is_overlay_shown, setIsOverlayShown]); - const initial_form = { token_name: '', read: false, @@ -98,8 +88,8 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown const toggleOverlay = () => setState({ is_overlay_shown: !state.is_overlay_shown }); - const validateFields = (values: FormikValues) => { - const errors: FormikErrors = {}; + const validateFields = (values: TApiTokenForm) => { + const errors: FormikErrors = {}; const token_name = values.token_name && values.token_name.trim(); if (!token_name) { @@ -121,9 +111,13 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown return errors; }; - const selectedTokenScope = (values: FormikValues) => - Object.keys(values).filter(item => item !== 'token_name' && values[item]); - const handleSubmit = async (values: FormikValues, { setSubmitting, setFieldError, resetForm }: any) => { + const selectedTokenScope = (values: TApiTokenForm) => + Object.keys(values).filter(item => item !== 'token_name' && Boolean(values[item as keyof TApiTokenForm])); + + const handleSubmit = async ( + values: TApiTokenForm, + { setSubmitting, setFieldError, resetForm }: FormikHelpers + ) => { const token_response = await WS.apiToken({ api_token: 1, new_token: values.token_name, @@ -181,7 +175,7 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown }, 500); }; - const { api_tokens, is_loading, is_success, error_message, is_overlay_shown } = state; + const { api_tokens, is_loading, is_success, error_message } = state; if (is_loading || is_switching) { return ; @@ -195,32 +189,24 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown api_tokens, toggleOverlay, deleteToken, - footer_ref, - overlay_ref, }; return ( -
+
- - {!is_app_settings && isMobile() && } + + {is_mobile && } {({ values, errors, isValid, dirty, - touched, handleChange, handleBlur, isSubmitting, - setFieldValue, setFieldTouched, }) => (
@@ -232,7 +218,6 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown )} @@ -346,11 +327,9 @@ const ApiToken = ({ footer_ref, is_app_settings, overlay_ref, setIsOverlayShown )} - {!is_app_settings && isDesktop() && } + {is_desktop && }
- {footer_ref && } - {overlay_ref && is_overlay_shown && } ); diff --git a/packages/account/src/Types/context.type.ts b/packages/account/src/Types/context.type.ts index f2aceee3e275..7757d7f368b8 100644 --- a/packages/account/src/Types/context.type.ts +++ b/packages/account/src/Types/context.type.ts @@ -3,7 +3,5 @@ import { TToken } from './common-prop.type'; export type TApiContext = { api_tokens: NonNullable | undefined; deleteToken: (token: string) => Promise; - footer_ref: Element | DocumentFragment | undefined; - overlay_ref: (...args: unknown[]) => unknown; toggleOverlay: () => void; }; diff --git a/packages/core/src/Stores/ui-store.js b/packages/core/src/Stores/ui-store.js index 34ab6547fb2f..844ce5e9923d 100644 --- a/packages/core/src/Stores/ui-store.js +++ b/packages/core/src/Stores/ui-store.js @@ -315,6 +315,7 @@ export default class UIStore extends BaseStore { init: action.bound, installWithDeferredPrompt: action.bound, is_account_switcher_disabled: computed, + is_desktop: computed, is_mobile: computed, is_tablet: computed, is_warning_scam_message_modal_visible: computed, @@ -492,6 +493,11 @@ export default class UIStore extends BaseStore { this.is_close_uk_account_modal_visible = is_open; } + get is_desktop() { + // TODO: remove tablet once there is a design for the specific size. + return this.is_tablet || this.screen_width > MAX_TABLET_WIDTH; + } + get is_mobile() { return this.screen_width <= MAX_MOBILE_WIDTH; } diff --git a/packages/stores/types.ts b/packages/stores/types.ts index 59fd8aba745f..658e4dec51b7 100644 --- a/packages/stores/types.ts +++ b/packages/stores/types.ts @@ -415,6 +415,7 @@ type TUiStore = { is_dark_mode_on: boolean; is_reports_visible: boolean; is_language_settings_modal_on: boolean; + is_desktop: boolean; is_mobile: boolean; sub_section_index: number; toggleShouldShowRealAccountsList: (value: boolean) => void; From 7fa5591dcc91df128566031141020e01b2bd3c31 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Wed, 26 Jul 2023 16:26:11 +0400 Subject: [PATCH 02/22] fix: added is_desktop to mocks also --- packages/stores/src/mockStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stores/src/mockStore.ts b/packages/stores/src/mockStore.ts index 18590524abc6..595ed6bb5280 100644 --- a/packages/stores/src/mockStore.ts +++ b/packages/stores/src/mockStore.ts @@ -329,6 +329,7 @@ const mock = (): TStores & { is_mock: boolean } => { toggleShouldShowRealAccountsList: jest.fn(), is_reset_trading_password_modal_visible: false, setResetTradingPasswordModalOpen: jest.fn(), + is_desktop: false, }, traders_hub: { closeModal: jest.fn(), From fb66efb03552fc40c695f658991ac111bae5d71a Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Wed, 26 Jul 2023 18:02:33 +0400 Subject: [PATCH 03/22] chore: remove comments --- .../api-token/_tests_/api-token.spec.js | 83 +++---------------- 1 file changed, 10 insertions(+), 73 deletions(-) diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.js b/packages/account/src/Components/api-token/_tests_/api-token.spec.js index fd963869f9af..063a70f0435d 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.js +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.js @@ -147,69 +147,6 @@ describe('', () => { expect(screen.queryByText(read_scope_description)).not.toBeInTheDocument(); }); - // isMobile.mockReturnValueOnce(true); - // isDesktop.mockReturnValueOnce(false); - - // render( - // - // - // - // ); - - // expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); - // expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); - // expect(await screen.findByText(trading_info_scope_description)).toBeInTheDocument(); - // expect(await screen.findByText(select_scopes_msg)).toBeInTheDocument(); - // expect(await screen.findByText(token_creation_description)).toBeInTheDocument(); - // expect(await screen.findByText(token_using_description)).toBeInTheDocument(); - // expect(await screen.findByText(trade_scope_description)).toBeInTheDocument(); - // expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); - // expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); - // expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); - // }); - - // it('should render ApiToken component with app_settings', async () => { - // mock_props.is_app_settings = true; - - // render( - // - // - // - // ); - - // await waitFor(() => { - // expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); - // }); - // }); - - // it('should render ApiTokenFooter, show and close ApiTokenOverlay after triggering links', async () => { - // const footer_portal_root_el = document.createElement('div'); - // document.body.appendChild(footer_portal_root_el); - // const overlay_portal_root_el = document.createElement('div'); - // document.body.appendChild(overlay_portal_root_el); - - // mock_props.footer_ref = footer_portal_root_el; - // mock_props.overlay_ref = overlay_portal_root_el; - - // render( - // - // - // - // ); - - // expect(await screen.findByText(learn_more_title)).toBeInTheDocument(); - // expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); - - // fireEvent.click(await screen.findByText(learn_more_title)); - // expect(await screen.findByText(our_access_description)).toBeInTheDocument(); - - // fireEvent.click(await screen.findByRole('button', { name: /done/i })); - // expect(screen.queryByText(our_access_description)).not.toBeInTheDocument(); - - // document.body.removeChild(footer_portal_root_el); - // document.body.removeChild(overlay_portal_root_el); - // }); - it('should choose checkbox, enter a valid value and create token', async () => { render( @@ -224,7 +161,7 @@ describe('', () => { const read_checkbox = checkboxes.find(card => card.name === 'read'); // Typecasting it since find can return undefined as well const token_name_input = await screen.findByLabelText('Token name'); - expect(checkboxes.length).toBe(5); + expect(checkboxes).toHaveLength(5); expect(create_btn).toBeDisabled(); expect(read_checkbox?.checked).toBeFalsy(); expect(token_name_input?.value).toBe(''); @@ -289,7 +226,7 @@ describe('', () => { expect(await screen.findByText('Second test token')).toBeInTheDocument(); const delete_btns_1 = screen.getAllByTestId('dt_token_delete_icon'); - expect(delete_btns_1.length).toBe(2); + expect(delete_btns_1).toHaveLength(2); fireEvent.click(delete_btns_1[0]); const no_btn_1 = screen.getByRole('button', { name: /cancel/i }); @@ -301,7 +238,7 @@ describe('', () => { }); const delete_btns_2 = await screen.findAllByTestId('dt_token_delete_icon'); - expect(delete_btns_2.length).toBe(2); + expect(delete_btns_2).toHaveLength(2); fireEvent.click(delete_btns_2[0]); const yes_btn_1 = screen.getByRole('button', { name: /yes, delete/i }); @@ -350,7 +287,7 @@ describe('', () => { expect(screen.queryByText('FirstTokenID')).not.toBeInTheDocument(); const toggle_visibility_btns = await screen.findAllByTestId('dt_toggle_visibility_icon'); - expect(toggle_visibility_btns.length).toBe(2); + expect(toggle_visibility_btns).toHaveLength(2); fireEvent.click(toggle_visibility_btns[0]); expect(screen.getByText('FirstTokenID')).toBeInTheDocument(); @@ -359,7 +296,7 @@ describe('', () => { expect(screen.getByText('SecondTokenID')).toBeInTheDocument(); const copy_btns_1 = await screen.findAllByTestId('dt_copy_token_icon'); - expect(copy_btns_1.length).toBe(2); + expect(copy_btns_1).toHaveLength(2); fireEvent.click(copy_btns_1[0]); expect(screen.queryByText(warning_msg)).not.toBeInTheDocument(); @@ -412,16 +349,16 @@ describe('', () => { ); - expect((await screen.findAllByText('Name')).length).toBe(3); - expect((await screen.findAllByText('Last Used')).length).toBe(3); - expect((await screen.findAllByText('Token')).length).toBe(3); - expect((await screen.findAllByText('Scopes')).length).toBe(3); + expect((await screen.findAllByText('Name'))).toHaveLength(3); + expect((await screen.findAllByText('Last Used'))).toHaveLength(3); + expect((await screen.findAllByText('Token'))).toHaveLength(3); + expect((await screen.findAllByText('Scopes'))).toHaveLength(3); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(await screen.findByText('Second test token')).toBeInTheDocument(); expect(screen.queryByText('Action')).not.toBeInTheDocument(); expect(screen.queryByText('SecondTokenID')).not.toBeInTheDocument(); const never_used = await screen.findAllByText('Never'); - expect(never_used.length).toBe(2); + expect(never_used).toHaveLength(2); }); it('should show token error if exists', async () => { From 6964daca836faf3715216b0b63bfc8bee81d962d Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Tue, 8 Aug 2023 09:22:41 +0400 Subject: [PATCH 04/22] test: testcases for api-token.tsx --- .../{api-token.spec.js => api-token.spec.tsx} | 105 ++++++++---------- .../src/Components/api-token/api-token.tsx | 3 - packages/account/src/Types/context.type.ts | 1 - 3 files changed, 49 insertions(+), 60 deletions(-) rename packages/account/src/Components/api-token/_tests_/{api-token.spec.js => api-token.spec.tsx} (83%) diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.js b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx similarity index 83% rename from packages/account/src/Components/api-token/_tests_/api-token.spec.js rename to packages/account/src/Components/api-token/_tests_/api-token.spec.tsx index 063a70f0435d..522a60ce75f2 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.js +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx @@ -1,19 +1,15 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { getPropertyValue, isDesktop, isMobile, useIsMounted, WS } from '@deriv/shared'; +import { getPropertyValue, useIsMounted, WS, isMobile, isDesktop } from '@deriv/shared'; import ApiToken from '../api-token'; import { StoreProvider, mockStore } from '@deriv/stores'; +import { FormikValues } from 'formik'; jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), getPropertyValue: jest.fn(() => []), - isDesktop: jest.fn(() => true), isMobile: jest.fn(() => false), useIsMounted: jest.fn().mockImplementation(() => () => true), -})); -jest.mock('@deriv/shared/src/services/ws-methods', () => ({ - __esModule: true, // this property makes it work, - default: 'mockedDefaultExport', WS: { apiToken: jest.fn(() => Promise.resolve({ @@ -32,7 +28,6 @@ jest.mock('@deriv/shared/src/services/ws-methods', () => ({ ), }, }, - useWS: () => undefined, })); jest.mock('@deriv/components', () => ({ @@ -40,9 +35,9 @@ jest.mock('@deriv/components', () => ({ Loading: () =>
Loading
, })); -let modal_root_el; +const modal_root_el = document.createElement('div'); + beforeAll(() => { - modal_root_el = document.createElement('div'); modal_root_el.setAttribute('id', 'modal_root'); document.body.appendChild(modal_root_el); }); @@ -70,8 +65,7 @@ describe('', () => { const your_access_description = "To access your mobile apps and other third-party apps, you'll first need to generate an API token."; - let store = mockStore(); - store = mockStore({ + const store = mockStore({ client: { is_switching: false, }, @@ -80,31 +74,11 @@ describe('', () => { is_mobile: true, }, }); - const mock_props = { - WS: { - apiToken: jest.fn(() => - Promise.resolve({ - api_token: { - tokens: [], - }, - }) - ), - authorized: { - apiToken: jest.fn(() => - Promise.resolve({ - api_token: { - tokens: [], - }, - }) - ), - }, - }, - }; it('should render ApiToken component without app_settings and footer', async () => { render( - + ); @@ -122,12 +96,33 @@ describe('', () => { expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); }); + it('should render ApiToken component without app_settings and footer for mobile', async () => { + (isMobile as jest.Mock).mockReturnValueOnce(true); + + render( + + + + ); + + expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); + expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); + expect(await screen.findByText(trading_info_scope_description)).toBeInTheDocument(); + expect(await screen.findByText(select_scopes_msg)).toBeInTheDocument(); + expect(await screen.findByText(token_creation_description)).toBeInTheDocument(); + expect(await screen.findByText(token_using_description)).toBeInTheDocument(); + expect(await screen.findByText(trade_scope_description)).toBeInTheDocument(); + expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); + expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); + expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); + }); + it('should not render ApiToken component if is not mounted', () => { - useIsMounted.mockImplementationOnce(() => () => false); + useIsMounted(); render( - + ); @@ -150,7 +145,7 @@ describe('', () => { it('should choose checkbox, enter a valid value and create token', async () => { render( - + ); @@ -158,8 +153,8 @@ describe('', () => { const checkboxes = await screen.findAllByRole('checkbox'); const create_btn = await screen.findByRole('button'); - const read_checkbox = checkboxes.find(card => card.name === 'read'); // Typecasting it since find can return undefined as well - const token_name_input = await screen.findByLabelText('Token name'); + const read_checkbox = checkboxes.find((card: FormikValues) => card.name === 'read') as HTMLInputElement; // Typecasting it since find can return undefined as well + const token_name_input = (await screen.findByLabelText('Token name')) as HTMLInputElement; expect(checkboxes).toHaveLength(5); expect(create_btn).toBeDisabled(); @@ -185,7 +180,7 @@ describe('', () => { expect(create_btn).toBeEnabled(); fireEvent.click(create_btn); - const updated_token_name_input = await screen.findByLabelText('Token name'); + const updated_token_name_input = (await screen.findByLabelText('Token name')) as HTMLInputElement; expect(updated_token_name_input.value).toBe(''); const createToken = WS.apiToken; @@ -195,7 +190,7 @@ describe('', () => { it('should render created tokens and trigger delete', async () => { jest.useFakeTimers(); - getPropertyValue.mockReturnValue([ + (getPropertyValue as jest.Mock).mockReturnValue([ { display_name: 'First test token', last_used: '', @@ -214,7 +209,7 @@ describe('', () => { render( - + ); @@ -258,9 +253,7 @@ describe('', () => { const warning_msg = 'Be careful who you share this token with. Anyone with this token can perform the following actions on your account behalf'; - document.execCommand = jest.fn(); - - getPropertyValue.mockReturnValue([ + (getPropertyValue as jest.Mock).mockReturnValue([ { display_name: 'First test token', last_used: '', @@ -279,7 +272,7 @@ describe('', () => { render( - + ); @@ -301,7 +294,9 @@ describe('', () => { fireEvent.click(copy_btns_1[0]); expect(screen.queryByText(warning_msg)).not.toBeInTheDocument(); - act(() => jest.advanceTimersByTime(2100)); + act(() => { + jest.advanceTimersByTime(2100); + }); expect(screen.queryByTestId('dt_token_copied_icon')).not.toBeInTheDocument(); fireEvent.click(copy_btns_1[1]); @@ -316,10 +311,8 @@ describe('', () => { }); it('should render created tokens for mobile', async () => { - isMobile.mockReturnValue(true); - isDesktop.mockReturnValue(false); - - getPropertyValue.mockReturnValue([ + (isMobile as jest.Mock).mockReturnValue(true); + (getPropertyValue as jest.Mock).mockReturnValue([ { display_name: 'First test token', last_used: '', @@ -345,14 +338,14 @@ describe('', () => { render( - + ); - expect((await screen.findAllByText('Name'))).toHaveLength(3); - expect((await screen.findAllByText('Last Used'))).toHaveLength(3); - expect((await screen.findAllByText('Token'))).toHaveLength(3); - expect((await screen.findAllByText('Scopes'))).toHaveLength(3); + expect(await screen.findAllByText('Name')).toHaveLength(3); + expect(await screen.findAllByText('Last Used')).toHaveLength(3); + expect(await screen.findAllByText('Token')).toHaveLength(3); + expect(await screen.findAllByText('Scopes')).toHaveLength(3); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(await screen.findByText('Second test token')).toBeInTheDocument(); expect(screen.queryByText('Action')).not.toBeInTheDocument(); @@ -369,11 +362,11 @@ describe('', () => { }) ); - getPropertyValue.mockReturnValue('New test error'); + (getPropertyValue as jest.Mock).mockReturnValue('New test error'); render( - + ); diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index 5bb1d9d509b0..bdcbd759c7f4 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -86,8 +86,6 @@ const ApiToken = () => { admin: false, }; - const toggleOverlay = () => setState({ is_overlay_shown: !state.is_overlay_shown }); - const validateFields = (values: TApiTokenForm) => { const errors: FormikErrors = {}; const token_name = values.token_name && values.token_name.trim(); @@ -187,7 +185,6 @@ const ApiToken = () => { const context_value = { api_tokens, - toggleOverlay, deleteToken, }; diff --git a/packages/account/src/Types/context.type.ts b/packages/account/src/Types/context.type.ts index 7757d7f368b8..d3deb11162c5 100644 --- a/packages/account/src/Types/context.type.ts +++ b/packages/account/src/Types/context.type.ts @@ -3,5 +3,4 @@ import { TToken } from './common-prop.type'; export type TApiContext = { api_tokens: NonNullable | undefined; deleteToken: (token: string) => Promise; - toggleOverlay: () => void; }; From 3ec58a419f9e761f5607c9ed43b9268a42e80626 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Tue, 8 Aug 2023 09:24:40 +0400 Subject: [PATCH 05/22] chore: remove isDesktop --- .../account/src/Components/api-token/_tests_/api-token.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx index 522a60ce75f2..b47fbfa5455a 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { getPropertyValue, useIsMounted, WS, isMobile, isDesktop } from '@deriv/shared'; +import { getPropertyValue, useIsMounted, WS, isMobile } from '@deriv/shared'; import ApiToken from '../api-token'; import { StoreProvider, mockStore } from '@deriv/stores'; import { FormikValues } from 'formik'; From 33c711ebb1f6147088617a053d4d16f4c6d5eea2 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Tue, 15 Aug 2023 13:37:54 +0400 Subject: [PATCH 06/22] chore: api_token_title check improvement --- .../src/Components/api-token/_tests_/api-token.spec.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx index b47fbfa5455a..c3ce47a436b3 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx @@ -341,11 +341,10 @@ describe('', () => { ); - - expect(await screen.findAllByText('Name')).toHaveLength(3); - expect(await screen.findAllByText('Last Used')).toHaveLength(3); - expect(await screen.findAllByText('Token')).toHaveLength(3); - expect(await screen.findAllByText('Scopes')).toHaveLength(3); + const api_token_titles = ['Name', 'Last Used', 'Token', 'Scopes']; + api_token_titles.forEach(async title => { + expect(await screen.findAllByText(title)).toHaveLength(3); + }); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(await screen.findByText('Second test token')).toBeInTheDocument(); expect(screen.queryByText('Action')).not.toBeInTheDocument(); From a1befbcee0876cbfed0e599339650a582b2e6746 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Thu, 17 Aug 2023 12:56:29 +0400 Subject: [PATCH 07/22] Revert "chore: api_token_title check improvement" This reverts commit 33c711ebb1f6147088617a053d4d16f4c6d5eea2. --- .../src/Components/api-token/_tests_/api-token.spec.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx index c3ce47a436b3..b47fbfa5455a 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx @@ -341,10 +341,11 @@ describe('', () => { ); - const api_token_titles = ['Name', 'Last Used', 'Token', 'Scopes']; - api_token_titles.forEach(async title => { - expect(await screen.findAllByText(title)).toHaveLength(3); - }); + + expect(await screen.findAllByText('Name')).toHaveLength(3); + expect(await screen.findAllByText('Last Used')).toHaveLength(3); + expect(await screen.findAllByText('Token')).toHaveLength(3); + expect(await screen.findAllByText('Scopes')).toHaveLength(3); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(await screen.findByText('Second test token')).toBeInTheDocument(); expect(screen.queryByText('Action')).not.toBeInTheDocument(); From 260b006f994bb521a3faaa9dfa55a26dad8619ce Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Tue, 22 Aug 2023 15:53:25 +0400 Subject: [PATCH 08/22] fix: defaultCheckbox type added --- packages/account/src/Components/api-token/api-token-card.tsx | 1 + .../src/components/composite-checkbox/composite-checkbox.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/account/src/Components/api-token/api-token-card.tsx b/packages/account/src/Components/api-token/api-token-card.tsx index d1bc536ba8d3..c7d54a4a01bb 100644 --- a/packages/account/src/Components/api-token/api-token-card.tsx +++ b/packages/account/src/Components/api-token/api-token-card.tsx @@ -20,6 +20,7 @@ const ApiTokenCard = ({ name, value, display_name, description, children }: Reac onChange={() => setFieldValue(name, !value)} value={value} className='api-token__checkbox' + defaultChecked={value} label={display_name} description={description} > diff --git a/packages/components/src/components/composite-checkbox/composite-checkbox.tsx b/packages/components/src/components/composite-checkbox/composite-checkbox.tsx index 5f4e759a333c..646a30a82cfd 100644 --- a/packages/components/src/components/composite-checkbox/composite-checkbox.tsx +++ b/packages/components/src/components/composite-checkbox/composite-checkbox.tsx @@ -24,7 +24,7 @@ const CompositeCheckbox = ({ description, children, ...props -}: React.PropsWithChildren) => { +}: React.PropsWithChildren>) => { const onClickContainer = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); From cc16344631f06b61d2c6586e54ba87f3fa4b417a Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Fri, 25 Aug 2023 12:48:11 +0400 Subject: [PATCH 09/22] fix: comments --- .../Components/api-token/api-token-card.tsx | 33 ++++++++----------- .../src/Components/api-token/api-token.tsx | 24 +++++++------- packages/account/src/Types/context.type.ts | 2 +- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/packages/account/src/Components/api-token/api-token-card.tsx b/packages/account/src/Components/api-token/api-token-card.tsx index c7d54a4a01bb..5c21781d37d8 100644 --- a/packages/account/src/Components/api-token/api-token-card.tsx +++ b/packages/account/src/Components/api-token/api-token-card.tsx @@ -1,33 +1,28 @@ import React from 'react'; -import { Field, FieldProps, useFormikContext } from 'formik'; +import { Field, FieldProps } from 'formik'; import { CompositeCheckbox } from '@deriv/components'; type TApiTokenCard = { description: string; display_name: string; name: string; - value: boolean; }; -const ApiTokenCard = ({ name, value, display_name, description, children }: React.PropsWithChildren) => { - const { setFieldValue } = useFormikContext(); +const ApiTokenCard = ({ name, display_name, description, children }: React.PropsWithChildren) => { return ( - {({ field }: FieldProps) => { - return ( - setFieldValue(name, !value)} - value={value} - className='api-token__checkbox' - defaultChecked={value} - label={display_name} - description={description} - > - {children} - - ); - }} + {({ field, form: { setFieldValue } }: FieldProps) => ( + setFieldValue(name, !field.value)} + className='api-token__checkbox' + label={display_name} + description={description} + > + {children} + + )} ); }; diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index bdcbd759c7f4..094d03e50fbd 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -38,9 +38,8 @@ type TApiTokenForm = { }; const ApiToken = () => { - const { client } = useStore(); + const { client, ui } = useStore(); const { is_switching } = client; - const { ui } = useStore(); const { is_desktop, is_mobile } = ui; const isMounted = useIsMounted(); const prev_is_switching = React.useRef(is_switching); @@ -61,6 +60,7 @@ const ApiToken = () => { is_delete_success: false, } ); + let timeout_deletetokens: NodeJS.Timeout | undefined; React.useEffect(() => { getApiTokens(); @@ -121,7 +121,6 @@ const ApiToken = () => { new_token: values.token_name, new_token_scopes: selectedTokenScope(values), }); - if (token_response.error) { setFieldError('token_name', token_response.error.message); } else if (isMounted()) { @@ -133,7 +132,6 @@ const ApiToken = () => { if (isMounted()) setState({ is_success: false }); }, 500); } - resetForm(); setSubmitting(false); }; @@ -162,13 +160,15 @@ const ApiToken = () => { const deleteToken = async (token: string) => { setState({ is_delete_loading: true }); + clearTimeout(timeout_deletetokens); + const token_response = await WS.authorized.apiToken({ api_token: 1, delete_token: token }); populateTokenResponse(token_response); if (isMounted()) setState({ is_delete_loading: false, is_delete_success: true }); - setTimeout(() => { + timeout_deletetokens = setTimeout(() => { if (isMounted()) setState({ is_delete_success: false }); }, 500); }; @@ -191,7 +191,7 @@ const ApiToken = () => { return ( -
+
{is_mobile && } @@ -201,6 +201,7 @@ const ApiToken = () => { errors, isValid, dirty, + touched, handleChange, handleBlur, isSubmitting, @@ -214,7 +215,6 @@ const ApiToken = () => {
{ /> { /> { /> { /> { 'Length of token name must be between 2 and 32 characters.' )} required - error={errors.token_name} + error={ + touched.token_name && errors.token_name + ? errors.token_name + : undefined + } /> )} diff --git a/packages/account/src/Types/context.type.ts b/packages/account/src/Types/context.type.ts index d3deb11162c5..714aa7e1d0e5 100644 --- a/packages/account/src/Types/context.type.ts +++ b/packages/account/src/Types/context.type.ts @@ -1,6 +1,6 @@ import { TToken } from './common-prop.type'; export type TApiContext = { - api_tokens: NonNullable | undefined; + api_tokens: NonNullable; deleteToken: (token: string) => Promise; }; From c5bd720a371f6c56800de35cadea793de40c144e Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Fri, 25 Aug 2023 15:33:55 +0400 Subject: [PATCH 10/22] fix: clearTimeout --- .../account/src/Components/api-token/api-token.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index 094d03e50fbd..b626ea641c95 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -60,12 +60,15 @@ const ApiToken = () => { is_delete_success: false, } ); - let timeout_deletetokens: NodeJS.Timeout | undefined; + const timeout_ref = React.useRef(); React.useEffect(() => { getApiTokens(); - return () => setState({ dispose_token: '' }); + return () => { + setState({ dispose_token: '' }); + clearTimeout(timeout_ref.current); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -160,15 +163,13 @@ const ApiToken = () => { const deleteToken = async (token: string) => { setState({ is_delete_loading: true }); - clearTimeout(timeout_deletetokens); - const token_response = await WS.authorized.apiToken({ api_token: 1, delete_token: token }); populateTokenResponse(token_response); if (isMounted()) setState({ is_delete_loading: false, is_delete_success: true }); - timeout_deletetokens = setTimeout(() => { + timeout_ref.current = setTimeout(() => { if (isMounted()) setState({ is_delete_success: false }); }, 500); }; From c4fcbfc8e066e51110ae60b5eaf6c57a6a059a83 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Wed, 30 Aug 2023 13:27:08 +0400 Subject: [PATCH 11/22] fix: is_desktop --- .../src/Components/api-token/_tests_/api-token.spec.tsx | 6 +++--- packages/core/src/Stores/ui-store.js | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx index b47fbfa5455a..9b38a1128dff 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx +++ b/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx @@ -1,9 +1,9 @@ import React from 'react'; +import { FormikValues } from 'formik'; import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { getPropertyValue, useIsMounted, WS, isMobile } from '@deriv/shared'; +import { isMobile, getPropertyValue, useIsMounted, WS } from '@deriv/shared'; +import { mockStore, StoreProvider } from '@deriv/stores'; import ApiToken from '../api-token'; -import { StoreProvider, mockStore } from '@deriv/stores'; -import { FormikValues } from 'formik'; jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), diff --git a/packages/core/src/Stores/ui-store.js b/packages/core/src/Stores/ui-store.js index 35d159a855f8..72623da1bd59 100644 --- a/packages/core/src/Stores/ui-store.js +++ b/packages/core/src/Stores/ui-store.js @@ -497,8 +497,7 @@ export default class UIStore extends BaseStore { } get is_desktop() { - // TODO: remove tablet once there is a design for the specific size. - return this.is_tablet || this.screen_width > MAX_TABLET_WIDTH; + return this.screen_width > MAX_MOBILE_WIDTH; } get is_mobile() { @@ -506,7 +505,7 @@ export default class UIStore extends BaseStore { } get is_tablet() { - return this.screen_width <= MAX_TABLET_WIDTH; + return MAX_MOBILE_WIDTH < this.screen_width && this.screen_width <= MAX_TABLET_WIDTH; } get is_account_switcher_disabled() { From d46925ebd45fb514677fd92f954f92bb326f38e2 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Wed, 30 Aug 2023 15:24:41 +0400 Subject: [PATCH 12/22] fix: is_desktop --- packages/core/src/Stores/ui-store.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/Stores/ui-store.js b/packages/core/src/Stores/ui-store.js index 72623da1bd59..98ec388eef59 100644 --- a/packages/core/src/Stores/ui-store.js +++ b/packages/core/src/Stores/ui-store.js @@ -505,7 +505,7 @@ export default class UIStore extends BaseStore { } get is_tablet() { - return MAX_MOBILE_WIDTH < this.screen_width && this.screen_width <= MAX_TABLET_WIDTH; + return this.screen_width <= MAX_TABLET_WIDTH; } get is_account_switcher_disabled() { From 20729aac5f4cc805c097a690c89ad6f7e5ddb747 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Thu, 31 Aug 2023 10:56:34 +0400 Subject: [PATCH 13/22] chore: remove styles --- packages/account/src/Components/api-token/api-token.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/account/src/Components/api-token/api-token.scss b/packages/account/src/Components/api-token/api-token.scss index c782679de0f5..6ce3f105e0ec 100644 --- a/packages/account/src/Components/api-token/api-token.scss +++ b/packages/account/src/Components/api-token/api-token.scss @@ -2,12 +2,6 @@ max-height: 100%; width: 100%; - @include tablet-up { - &--app-settings { - padding: 2.4rem; - } - } - & .dc-timeline__container { width: 100%; } From 55a8a515ef7ee8e0735b8f150a2a553e3669dcd7 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Fri, 1 Sep 2023 10:34:35 +0400 Subject: [PATCH 14/22] chore: Localize component --- .../Components/api-token/api-token-card.tsx | 2 +- .../src/Components/api-token/api-token.tsx | 38 +++++++++---------- .../composite-checkbox/composite-checkbox.tsx | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/account/src/Components/api-token/api-token-card.tsx b/packages/account/src/Components/api-token/api-token-card.tsx index 5c21781d37d8..bfec08a7c9ce 100644 --- a/packages/account/src/Components/api-token/api-token-card.tsx +++ b/packages/account/src/Components/api-token/api-token-card.tsx @@ -3,7 +3,7 @@ import { Field, FieldProps } from 'formik'; import { CompositeCheckbox } from '@deriv/components'; type TApiTokenCard = { - description: string; + description: JSX.Element; display_name: string; name: string; }; diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index b626ea641c95..e844e2ae0b98 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -4,7 +4,7 @@ import { Formik, Form, Field, FormikErrors, FieldProps, FormikHelpers } from 'fo import { Timeline, Input, Button, ThemedScrollbars, Loading } from '@deriv/components'; import InlineNoteWithIcon from '../inline-note-with-icon'; import { getPropertyValue, useIsMounted, WS } from '@deriv/shared'; -import { localize } from '@deriv/translations'; +import { Localize, localize } from '@deriv/translations'; import LoadErrorMessage from 'Components/load-error-message'; import ApiTokenArticle from './api-token-article'; import ApiTokenCard from './api-token-card'; @@ -217,43 +217,43 @@ const ApiToken = () => { + } /> + } /> + } /> + } /> + } > + } title={localize('Note')} /> diff --git a/packages/components/src/components/composite-checkbox/composite-checkbox.tsx b/packages/components/src/components/composite-checkbox/composite-checkbox.tsx index 646a30a82cfd..31e3ab61973f 100644 --- a/packages/components/src/components/composite-checkbox/composite-checkbox.tsx +++ b/packages/components/src/components/composite-checkbox/composite-checkbox.tsx @@ -11,7 +11,7 @@ type TCompositeCheckbox = { className?: string; label: string; id?: string; - description: string; + description: JSX.Element; }; const CompositeCheckbox = ({ From 4d37aa06a163e3b4bd5054ee7c78b9c9fb18f2b6 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Fri, 1 Sep 2023 16:33:43 +0400 Subject: [PATCH 15/22] chore: introduce loop for api token card --- .../src/Components/api-token/api-token.tsx | 71 ++++++++----------- .../src/Constants/api-token-card-details.tsx | 40 +++++++++++ 2 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 packages/account/src/Constants/api-token-card-details.tsx diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index e844e2ae0b98..bec350c57cc1 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -12,6 +12,7 @@ import ApiTokenTable from './api-token-table'; import ApiTokenContext from './api-token-context'; import { TToken } from 'Types'; import { observer, useStore } from '@deriv/stores'; +import { getApiTokenCardDetails } from 'Constants/api-token-card-details'; const MIN_TOKEN = 2; const MAX_TOKEN = 32; @@ -189,6 +190,8 @@ const ApiToken = () => { deleteToken, }; + const api_token_card_array = getApiTokenCardDetails(); + return ( @@ -214,49 +217,31 @@ const ApiToken = () => { item_title={localize('Select scopes based on the access you need.')} >
- - } - /> - - } - /> - - } - /> - - } - /> - - } - > - - } - title={localize('Note')} - /> - + {api_token_card_array.map(card => + card.name === 'admin' ? ( + + + } + title={localize('Note')} + /> + + ) : ( + + ) + )}
[ + { + name: 'read', + display_name: localize('Read'), + description: ( + + ), + }, + { + name: 'trade', + display_name: localize('Trade'), + description: ( + + ), + }, + { + name: 'payments', + display_name: localize('Payments'), + description: ( + + ), + }, + { + name: 'trading_information', + display_name: localize('Trading information'), + description: ( + + ), + }, + { + name: 'admin', + display_name: localize('Admin'), + description: ( + + ), + }, +]; From 2aec880c2f48720bf359492669c52d2ec5be5702 Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Fri, 1 Sep 2023 18:07:06 +0400 Subject: [PATCH 16/22] chore: refactor api token card --- .../src/Components/api-token/api-token.tsx | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index bec350c57cc1..66e213765ace 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -217,14 +217,14 @@ const ApiToken = () => { item_title={localize('Select scopes based on the access you need.')} >
- {api_token_card_array.map(card => - card.name === 'admin' ? ( - + {api_token_card_array.map(card => ( + + {card.name === 'admin' && ( { } title={localize('Note')} /> - - ) : ( - - ) - )} + )} + + ))}
Date: Tue, 5 Sep 2023 18:41:11 +0400 Subject: [PATCH 17/22] feat: :art: incorporated hooks for API token --- packages/account/src/App.tsx | 13 +- .../src/Components/api-token/api-token.tsx | 152 +++++++----------- packages/api/src/hooks/index.ts | 2 + packages/api/src/hooks/useGetApiToken.ts | 12 ++ packages/api/src/hooks/useSetApiToken.ts | 19 +++ 5 files changed, 96 insertions(+), 102 deletions(-) create mode 100644 packages/api/src/hooks/useGetApiToken.ts create mode 100644 packages/api/src/hooks/useSetApiToken.ts diff --git a/packages/account/src/App.tsx b/packages/account/src/App.tsx index 23cbccb72a8e..6e8a413b434f 100644 --- a/packages/account/src/App.tsx +++ b/packages/account/src/App.tsx @@ -4,6 +4,7 @@ import ResetTradingPassword from './Containers/reset-trading-password'; import { setWebsocket } from '@deriv/shared'; import { StoreProvider } from '@deriv/stores'; import { TCoreStores } from '@deriv/stores/types'; +import APIProvider from '../../api/src/APIProvider'; // TODO: add correct types for WS after implementing them type TAppProps = { @@ -20,11 +21,13 @@ const App = ({ passthrough }: TAppProps) => { const { notification_messages_ui: Notifications } = root_store.ui; return ( - - {Notifications && } - - - + + + {Notifications && } + + + + ); }; diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx index 66e213765ace..5989b2e80513 100644 --- a/packages/account/src/Components/api-token/api-token.tsx +++ b/packages/account/src/Components/api-token/api-token.tsx @@ -1,32 +1,27 @@ import React from 'react'; import classNames from 'classnames'; import { Formik, Form, Field, FormikErrors, FieldProps, FormikHelpers } from 'formik'; +import { useSetApiToken, useGetApiToken } from '@deriv/api'; +import { APITokenResponse, ApiToken as TApitoken } from '@deriv/api-types'; import { Timeline, Input, Button, ThemedScrollbars, Loading } from '@deriv/components'; -import InlineNoteWithIcon from '../inline-note-with-icon'; -import { getPropertyValue, useIsMounted, WS } from '@deriv/shared'; +import { getPropertyValue } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; import { Localize, localize } from '@deriv/translations'; +import { TToken } from 'Types'; import LoadErrorMessage from 'Components/load-error-message'; +import { getApiTokenCardDetails } from 'Constants/api-token-card-details'; import ApiTokenArticle from './api-token-article'; import ApiTokenCard from './api-token-card'; import ApiTokenTable from './api-token-table'; import ApiTokenContext from './api-token-context'; -import { TToken } from 'Types'; -import { observer, useStore } from '@deriv/stores'; -import { getApiTokenCardDetails } from 'Constants/api-token-card-details'; +import InlineNoteWithIcon from '../inline-note-with-icon'; const MIN_TOKEN = 2; const MAX_TOKEN = 32; type AptTokenState = { api_tokens: NonNullable; - is_loading: boolean; - is_success: boolean; - is_overlay_shown: boolean; error_message: string; - show_delete: boolean; - dispose_token: string; - is_delete_loading: boolean; - is_delete_success: boolean; }; type TApiTokenForm = { @@ -42,8 +37,10 @@ const ApiToken = () => { const { client, ui } = useStore(); const { is_switching } = client; const { is_desktop, is_mobile } = ui; - const isMounted = useIsMounted(); - const prev_is_switching = React.useRef(is_switching); + + const { updated_api_token_data, update, isSuccess, isLoading } = useSetApiToken(); + const { api_token_data, isSuccess: is_api_token_data_fetched } = useGetApiToken(); + const [state, setState] = React.useReducer( (prev_state: Partial, value: Partial) => ({ ...prev_state, @@ -51,35 +48,33 @@ const ApiToken = () => { }), { api_tokens: [], - is_loading: true, - is_success: false, - is_overlay_shown: false, error_message: '', - show_delete: false, - dispose_token: '', - is_delete_loading: false, - is_delete_success: false, } ); - const timeout_ref = React.useRef(); - - React.useEffect(() => { - getApiTokens(); - return () => { - setState({ dispose_token: '' }); - clearTimeout(timeout_ref.current); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps + const populateTokenResponse = React.useCallback((response: APITokenResponse) => { + if (response.error) { + setState({ + error_message: getPropertyValue(response, ['error', 'message']), + }); + } else { + setState({ + api_tokens: getPropertyValue(response, ['api_token', 'tokens']), + }); + } }, []); React.useEffect(() => { - if (prev_is_switching.current !== is_switching) { - prev_is_switching.current = is_switching; - getApiTokens(); + if (is_api_token_data_fetched) { + populateTokenResponse(api_token_data as APITokenResponse); + } + }, [api_token_data, is_api_token_data_fetched, populateTokenResponse]); + + React.useEffect(() => { + if (isSuccess) { + populateTokenResponse(updated_api_token_data as APITokenResponse); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [is_switching]); + }, [isSuccess, populateTokenResponse, updated_api_token_data]); const initial_form = { token_name: '', @@ -114,70 +109,33 @@ const ApiToken = () => { }; const selectedTokenScope = (values: TApiTokenForm) => - Object.keys(values).filter(item => item !== 'token_name' && Boolean(values[item as keyof TApiTokenForm])); + Object.keys(values).filter( + item => item !== 'token_name' && Boolean(values[item as keyof TApiTokenForm]) + ) as NonNullable[0]['scopes']>; - const handleSubmit = async ( - values: TApiTokenForm, - { setSubmitting, setFieldError, resetForm }: FormikHelpers - ) => { - const token_response = await WS.apiToken({ - api_token: 1, + const handleSubmit = (values: TApiTokenForm, { setSubmitting, resetForm }: FormikHelpers) => { + update({ new_token: values.token_name, new_token_scopes: selectedTokenScope(values), }); - if (token_response.error) { - setFieldError('token_name', token_response.error.message); - } else if (isMounted()) { - setState({ - is_success: true, - api_tokens: getPropertyValue(token_response, ['api_token', 'tokens']), - }); - setTimeout(() => { - if (isMounted()) setState({ is_success: false }); - }, 500); - } + // if (token_response.error) { + // setFieldError('token_name', token_response.error.message); + // } else if (isMounted()) { + // setState({ + // api_tokens: getPropertyValue(token_response, ['api_token', 'tokens']), + // }); + // } resetForm(); setSubmitting(false); }; - const populateTokenResponse = (response: import('@deriv/api-types').APITokenResponse) => { - if (!isMounted()) return; - if (response.error) { - setState({ - is_loading: false, - error_message: getPropertyValue(response, ['error', 'message']), - }); - } else { - setState({ - is_loading: false, - api_tokens: getPropertyValue(response, ['api_token', 'tokens']), - }); - } - }; - - const getApiTokens = async () => { - setState({ is_loading: true }); - const token_response = await WS.authorized.apiToken({ api_token: 1 }); - populateTokenResponse(token_response); - }; - - const deleteToken = async (token: string) => { - setState({ is_delete_loading: true }); - - const token_response = await WS.authorized.apiToken({ api_token: 1, delete_token: token }); - - populateTokenResponse(token_response); - - if (isMounted()) setState({ is_delete_loading: false, is_delete_success: true }); - - timeout_ref.current = setTimeout(() => { - if (isMounted()) setState({ is_delete_success: false }); - }, 500); + const deleteToken = (token: string) => { + update({ delete_token: token }); }; - const { api_tokens, is_loading, is_success, error_message } = state; + const { api_tokens, error_message } = state; - if (is_loading || is_switching) { + if (is_switching) { return ; } @@ -199,7 +157,7 @@ const ApiToken = () => {
{is_mobile && } - + {({ values, errors, @@ -274,19 +232,19 @@ const ApiToken = () => { 'dc-btn__button-group', 'da-api-token__button', { - 'da-api-token__button--success': is_success, + 'da-api-token__button--success': isSuccess, } )} type='submit' - is_disabled={ - !dirty || - isSubmitting || - !isValid || - !selectedTokenScope(values).length - } + // is_disabled={ + // !dirty || + // isSubmitting || + // !isValid || + // !selectedTokenScope(values).length + // } has_effect is_loading={isSubmitting} - is_submit_success={is_success} + is_submit_success={isLoading} text={localize('Create')} primary large diff --git a/packages/api/src/hooks/index.ts b/packages/api/src/hooks/index.ts index b9a906787650..bf79764990db 100644 --- a/packages/api/src/hooks/index.ts +++ b/packages/api/src/hooks/index.ts @@ -14,3 +14,5 @@ export { default as useTradingAccountsList } from './useTradingAccountsList'; export { default as useTradingPlatformAccounts } from './useTradingPlatformAccounts'; export { default as useTradingPlatformAvailableAccounts } from './useTradingPlatformAvailableAccounts'; export { default as useWalletAccountsList } from './useWalletAccountsList'; +export { default as useGetApiToken } from './useGetApiToken'; +export { default as useSetApiToken } from './useSetApiToken'; diff --git a/packages/api/src/hooks/useGetApiToken.ts b/packages/api/src/hooks/useGetApiToken.ts new file mode 100644 index 000000000000..97b8e572c52c --- /dev/null +++ b/packages/api/src/hooks/useGetApiToken.ts @@ -0,0 +1,12 @@ +import useFetch from '../useFetch'; + +const useGetApiToken = () => { + const { data, ...rest } = useFetch('api_token'); + + return { + api_token_data: data, + ...rest, + }; +}; + +export default useGetApiToken; diff --git a/packages/api/src/hooks/useSetApiToken.ts b/packages/api/src/hooks/useSetApiToken.ts new file mode 100644 index 000000000000..6e669be51da1 --- /dev/null +++ b/packages/api/src/hooks/useSetApiToken.ts @@ -0,0 +1,19 @@ +import React from 'react'; +import useRequest from '../useRequest'; + +type TAPITokenPayload = NonNullable< + NonNullable>['mutate']>>[0]>['payload'] +>; +const useSetApiToken = () => { + const { data, mutate, ...rest } = useRequest('api_token'); + + const update = React.useCallback((payload?: TAPITokenPayload) => mutate({ payload }), [mutate]); + + return { + updated_api_token_data: data, + update, + ...rest, + }; +}; + +export default useSetApiToken; From ade390e2d84f662f1f63282f0cfc7bde4f0cc9cd Mon Sep 17 00:00:00 2001 From: Likhith Kolayari Date: Mon, 11 Sep 2023 12:07:13 +0400 Subject: [PATCH 18/22] feat: :art: incorporated hooks --- packages/account/src/App.tsx | 6 +- .../account-limits/account-limits.tsx | 2 +- .../__tests__/api-token-article.spec.tsx | 13 + .../__tests__/api-token-card.spec.tsx | 35 +++ .../__tests__/api-token-clipboard.spec.tsx | 81 ++++++ .../api-token-delete-button.spec.tsx | 97 +++++++ .../api-token-table-row-cell.spec.tsx | 29 ++ .../api-token-table-row-header.spec.tsx | 14 + .../api-token-table-row-scopes-cell.spec.tsx | 15 + .../api-token-table-row-token-cell.spec.tsx | 26 ++ .../__tests__/api-token-table-row.spec.tsx | 33 +++ .../__tests__/api-token-table.spec.tsx | 90 ++++++ .../api-token/api-token-article.tsx | 4 +- .../Components/api-token/api-token-card.tsx | 2 +- .../api-token/api-token-clipboard.tsx | 21 +- .../Components/api-token/api-token-context.ts | 6 +- .../api-token/api-token-delete-button.tsx | 37 ++- .../api-token/api-token-table-row-header.tsx | 2 +- .../api-token-table-row-token-cell.tsx | 16 +- .../Components/api-token/api-token-table.tsx | 8 +- .../src/Components/api-token/api-token.tsx | 272 ------------------ .../account/src/Components/api-token/index.ts | 14 +- .../src/Components/article/article.tsx | 2 +- .../inline-note-with-icon.tsx | 2 +- .../src/Constants/api-token-card-details.tsx | 5 + .../ApiToken/__tests__}/api-token.spec.tsx | 227 ++++++++------- .../Security/ApiToken}/api-token.scss | 0 .../Sections/Security/ApiToken/api-token.tsx | 266 ++++++++++++++++- packages/account/src/Types/context.type.ts | 1 + .../src/hooks/__tests__/useApiToken.spec.tsx | 58 ++++ packages/api/src/hooks/index.ts | 3 +- packages/api/src/hooks/useApiToken.ts | 55 ++++ .../src/components/popup/popup-overlay.tsx | 2 +- 33 files changed, 995 insertions(+), 449 deletions(-) create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-article.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-card.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-clipboard.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-delete-button.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-table-row-cell.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-table-row-header.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-table-row-scopes-cell.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-table-row-token-cell.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-table-row.spec.tsx create mode 100644 packages/account/src/Components/api-token/__tests__/api-token-table.spec.tsx delete mode 100644 packages/account/src/Components/api-token/api-token.tsx rename packages/account/src/{Components/api-token/_tests_ => Sections/Security/ApiToken/__tests__}/api-token.spec.tsx (73%) rename packages/account/src/{Components/api-token => Sections/Security/ApiToken}/api-token.scss (100%) create mode 100644 packages/api/src/hooks/__tests__/useApiToken.spec.tsx create mode 100644 packages/api/src/hooks/useApiToken.ts diff --git a/packages/account/src/App.tsx b/packages/account/src/App.tsx index 21d1a381c5b6..6e8a413b434f 100644 --- a/packages/account/src/App.tsx +++ b/packages/account/src/App.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { APIProvider } from '@deriv/api'; +import Routes from './Containers/routes'; +import ResetTradingPassword from './Containers/reset-trading-password'; import { setWebsocket } from '@deriv/shared'; import { StoreProvider } from '@deriv/stores'; import { TCoreStores } from '@deriv/stores/types'; -import Routes from './Containers/routes'; -import ResetTradingPassword from './Containers/reset-trading-password'; +import APIProvider from '../../api/src/APIProvider'; // TODO: add correct types for WS after implementing them type TAppProps = { diff --git a/packages/account/src/Components/account-limits/account-limits.tsx b/packages/account/src/Components/account-limits/account-limits.tsx index 0bb41406c1ab..2379367810d6 100644 --- a/packages/account/src/Components/account-limits/account-limits.tsx +++ b/packages/account/src/Components/account-limits/account-limits.tsx @@ -295,7 +295,7 @@ const AccountLimits = observer( color='colored-background' size={isMobile() ? 'xxxs' : 'xxs'} > - {localize('Verify')} + diff --git a/packages/account/src/Components/api-token/__tests__/api-token-article.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-article.spec.tsx new file mode 100644 index 000000000000..c85e3ddc3130 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-article.spec.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import ApiTokenArticle from '../api-token-article'; + +it('should render ApiTokenArticle', () => { + render(); + expect(screen.getByText('API token')); + expect( + screen.getByText( + /To access your mobile apps and other third-party apps, you'll first need to generate an API token./i + ) + ).toBeInTheDocument(); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-card.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-card.spec.tsx new file mode 100644 index 000000000000..a5bccfe7c186 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-card.spec.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Formik, Form } from 'formik'; +import { screen, render } from '@testing-library/react'; +import ApiTokenCard from '../api-token-card'; + +describe('', () => { + const mock_props = { + name: 'Api_token_card_test_case', + value: false, + display_name:
API Token Card
, + description:
API Token Description
, + }; + + const renderComponent = (children?: JSX.Element) => { + render( + + + {children} + + + ); + }; + + it('should render ApiTokenCard', () => { + renderComponent(); + expect(screen.getByText('API Token Card')).toBeInTheDocument(); + expect(screen.getByText('API Token Description')).toBeInTheDocument(); + }); + + it('should render ApiTokenCard with children', () => { + const children =
API Token Children
; + renderComponent(children); + expect(screen.getByText('API Token Children')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-clipboard.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-clipboard.spec.tsx new file mode 100644 index 000000000000..f9313f935317 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-clipboard.spec.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { screen, render, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ApiTokenClipboard from '../api-token-clipboard'; + +const modal_root_el = document.createElement('div'); +modal_root_el.setAttribute('id', 'modal_root'); +document.body.appendChild(modal_root_el); + +describe('ApiTokenClipboard', () => { + const mock_props = { + scopes: ['read', 'trade', 'Admin'], + text_copy: 'Text Copy', + info_message: 'Copy this token', + success_message: 'Success Message', + }; + + it('should render ApiTokenClipboard with the copy icon', () => { + render(); + expect(screen.getByTestId('dt_copy_token_icon')).toBeInTheDocument(); + }); + + it('should display "Copy this token" message when mouse enters', () => { + render(); + const copy_icon = screen.getByTestId('dt_copy_token_icon'); + userEvent.hover(copy_icon); + expect(screen.getByText('Copy this token')).toBeInTheDocument(); + }); + + it('should remove "Copy this token" message when mouse leaves', () => { + render(); + const copy_icon = screen.getByTestId('dt_copy_token_icon'); + userEvent.hover(copy_icon); + expect(screen.getByText('Copy this token')).toBeInTheDocument(); + userEvent.unhover(copy_icon); + expect(screen.queryByText('Copy this token')).not.toBeInTheDocument(); + }); + + it('should display Popup Modal when user clicks on copy_icon', () => { + render(); + const copy_icon = screen.getByTestId('dt_copy_token_icon'); + userEvent.click(copy_icon); + expect( + screen.getByText( + 'Be careful who you share this token with. Anyone with this token can perform the following actions on your account behalf' + ) + ).toBeInTheDocument(); + expect(screen.getByText('Add accounts')).toBeInTheDocument(); + expect(screen.getByText('Create or delete API tokens for trading and withdrawals')).toBeInTheDocument(); + expect(screen.getByText('Modify account settings')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument(); + }); + + it('should remove Popup modal when user clicks on OK', async () => { + render(); + const copy_icon = screen.getByTestId('dt_copy_token_icon'); + userEvent.click(copy_icon); + expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument(); + const ok_button = screen.getByRole('button', { name: 'OK' }); + userEvent.click(ok_button); + await waitFor(() => { + expect(screen.queryByText('Add accounts')).not.toBeInTheDocument(); + }); + }); + + it('should not display Popup Modal when user clicks on copy_icon with no Admin scope', () => { + mock_props.scopes = ['read', 'trade']; + render(); + const copy_icon = screen.getByTestId('dt_copy_token_icon'); + userEvent.click(copy_icon); + expect( + screen.queryByText( + 'Be careful who you share this token with. Anyone with this token can perform the following actions on your account behalf' + ) + ).not.toBeInTheDocument(); + expect(screen.queryByText('Add accounts')).not.toBeInTheDocument(); + expect(screen.queryByText('Create or delete API tokens for trading and withdrawals')).not.toBeInTheDocument(); + expect(screen.queryByText('Modify account settings')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'OK' })).not.toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-delete-button.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-delete-button.spec.tsx new file mode 100644 index 000000000000..e793a005c07d --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-delete-button.spec.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { screen, render, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ApiTokenContext from '../api-token-context'; +import ApiTokenDeleteButton from '../api-token-delete-button'; + +const modal_root_el = document.createElement('div'); +modal_root_el.setAttribute('id', 'modal_root'); +document.body.appendChild(modal_root_el); + +describe('ApiTokenDeleteButton', () => { + const mock_props = { + api_tokens: [ + { + display_name: '', + last_used: '', + scopes: [], + token: '', + }, + ], + deleteToken: jest.fn(() => Promise.resolve()), + footer_ref: document.createElement('div'), + overlay_ref: document.createElement('div'), + toggleOverlay: jest.fn(), + }; + const mock_token = { + token: { + display_name: 'Token 1', + last_used: '12/31/2022', + scopes: ['read', 'trade'], + token: '1234567', + }, + }; + + const renderAPIDeleteButton = () => { + render( + + + + ); + }; + + it('should render ApiTokenDeleteButton', () => { + renderAPIDeleteButton(); + expect(screen.getByTestId('dt_token_delete_icon')).toBeInTheDocument(); + expect(screen.queryByText('Delete this token')).not.toBeInTheDocument(); + }); + + it('should display Delete this token when mouse enter', () => { + renderAPIDeleteButton(); + const delete_icon = screen.getByTestId('dt_token_delete_icon'); + userEvent.hover(delete_icon); + expect(screen.getByText('Delete this token')).toBeInTheDocument(); + }); + + it('should not display Delete this token when mouse leave', () => { + renderAPIDeleteButton(); + const delete_icon = screen.getByTestId('dt_token_delete_icon'); + userEvent.hover(delete_icon); + expect(screen.getByText('Delete this token')).toBeInTheDocument(); + userEvent.unhover(delete_icon); + expect(screen.queryByText('Delete this token')).not.toBeInTheDocument(); + }); + + it('should display Popup when delete icon is clicked', () => { + renderAPIDeleteButton(); + const delete_icon = screen.getByTestId('dt_token_delete_icon'); + userEvent.click(delete_icon); + expect(screen.getByText('Delete token')).toBeInTheDocument(); + expect(screen.getByText('Are you sure you want to delete this token?')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Yes, delete' })).toBeInTheDocument(); + }); + + it('should close the modal when clicked on Cancel', async () => { + renderAPIDeleteButton(); + const delete_icon = screen.getByTestId('dt_token_delete_icon'); + userEvent.click(delete_icon); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + const cancel_button = screen.getByRole('button', { name: 'Cancel' }); + userEvent.click(cancel_button); + await waitFor(() => expect(screen.queryByText('Delete token')).not.toBeInTheDocument()); + expect(screen.queryByText('Are you sure you want to delete this token?')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Yes, delete' })).not.toBeInTheDocument(); + }); + + it('should should trigger deleteToken when clicked on Yes, delete', () => { + renderAPIDeleteButton(); + const delete_icon = screen.getByTestId('dt_token_delete_icon'); + userEvent.click(delete_icon); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); + const delete_token_button = screen.getByRole('button', { name: 'Yes, delete' }); + userEvent.click(delete_token_button); + expect(mock_props.deleteToken).toBeCalled(); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-table-row-cell.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-table-row-cell.spec.tsx new file mode 100644 index 000000000000..8315da9bc568 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-table-row-cell.spec.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import ApiTokenTableRowCell from '../api-token-table-row-cell'; + +describe('ApiTokenTableRowCell', () => { + const mock_props = { + className: 'api_token_table_row_cell', + should_bypass_text: false, + }; + const children = 'Api Table Row Cell'; + it('should render ApiTokenTableRowCell', () => { + render({children}); + const text_message = screen.getByText(children); + expect(text_message).toBeInTheDocument(); + expect(text_message).toHaveClass('dc-text'); + }); + + it('should render ApiTokenTableRowCell with table data if should_bypass_text is true', () => { + render( + + {children} + + ); + const text_message = screen.getByText(children); + expect(text_message).toBeInTheDocument(); + expect(text_message).not.toHaveClass('dc-text'); + expect(text_message).toHaveClass('da-api-token__table-cell api_token_table_row_cell'); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-table-row-header.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-table-row-header.spec.tsx new file mode 100644 index 000000000000..939630083953 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-table-row-header.spec.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import ApiTokenTableRowHeader from '../api-token-table-row-header'; + +describe('ApiTokenTableRowHeader', () => { + const mock_props = { + text: 'Api Token Table Row Header', + }; + + it('should render ApiTokenTableRowHeader', () => { + render(); + expect(screen.getByText('Api Token Table Row Header')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-table-row-scopes-cell.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-table-row-scopes-cell.spec.tsx new file mode 100644 index 000000000000..7e6b4b7e1a9b --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-table-row-scopes-cell.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import ApiTokenTableRowScopesCell from '../api-token-table-row-scopes-cell'; + +describe('ApiTokenTableRowScopeCell', () => { + const mock_props = { + scopes: ['api scope 1', 'api scope 2'], + }; + + it('should render ApiTokenTableRowScopesCell', () => { + render(); + expect(screen.getByText('api scope 1')).toBeInTheDocument(); + expect(screen.getByText('api scope 2')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-table-row-token-cell.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-table-row-token-cell.spec.tsx new file mode 100644 index 000000000000..a634e33fded1 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-table-row-token-cell.spec.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ApiTokenTableRowTokenCell from '../api-token-table-row-token-cell'; + +describe('ApiTokenTableRowTokenCell', () => { + const mock_props = { + token: '1234567', + scopes: ['api scope 1', 'api scope 2'], + }; + it('should render ApiTokenTableRowTokenCell', () => { + render(); + expect(screen.getByTestId('dt_hidden_tokens')).toBeInTheDocument(); + expect(screen.getByTestId('dt_copy_token_icon')).toBeInTheDocument(); + expect(screen.getByTestId('dt_toggle_visibility_icon')).toBeInTheDocument(); + expect(screen.getByTestId('dt_toggle_visibility_icon')).toHaveClass('dc-icon da-api-token__visibility-icon'); + }); + + it('should show token after clicking on dt_toggle_visibility_icon', () => { + render(); + const toggle_token_button = screen.getByTestId('dt_toggle_visibility_icon'); + userEvent.click(toggle_token_button); + expect(screen.getByText('1234567')).toBeInTheDocument(); + expect(screen.getByTestId('dt_toggle_visibility_icon')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-table-row.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-table-row.spec.tsx new file mode 100644 index 000000000000..ef70370f0d0b --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-table-row.spec.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import ApiTokenTableRow from '../api-token-table-row'; + +describe('ApiTokenTableRow', () => { + const mock_props = { + token: { + display_name: 'Api Token', + last_used: '31/12/2022', + scopes: ['Api scope 1', 'Api scope 2'], + token: '1234567', + }, + }; + it('should render ApiTokenTableRow', () => { + render(); + const texts = ['Api Token', 'Api scope 1', 'Api scope 2', '31/12/2022']; + + const test_ids = [ + 'dt_hidden_tokens', + 'dt_copy_token_icon', + 'dt_toggle_visibility_icon', + 'dt_token_delete_icon', + ]; + + texts.forEach(text => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + + test_ids.forEach(test_id => { + expect(screen.getByTestId(test_id)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/account/src/Components/api-token/__tests__/api-token-table.spec.tsx b/packages/account/src/Components/api-token/__tests__/api-token-table.spec.tsx new file mode 100644 index 000000000000..f63938776044 --- /dev/null +++ b/packages/account/src/Components/api-token/__tests__/api-token-table.spec.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import { isMobile } from '@deriv/shared'; +import ApiTokenContext from '../api-token-context'; +import ApiTokenTable from '../api-token-table'; + +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + isMobile: jest.fn(() => false), + isDesktop: jest.fn(() => true), +})); + +describe('ApiTokenTable', () => { + const mock_props = { + api_tokens: [ + { + display_name: 'Token 1', + token: 'token_1', + scopes: ['read', 'trade', 'payments', 'admin', 'trading_information', 'write'], + last_used: '2023-07-28T12:00:00Z', + }, + ], + deleteToken: jest.fn(), + footer_ref: document.createElement('div'), + overlay_ref: document.createElement('div'), + toggleOverlay: jest.fn(), + }; + + let expectedTexts = ['']; + + beforeEach(() => { + expectedTexts = [ + 'Name', + 'Token', + 'Scopes', + 'Token 1', + 'Read', + 'Trade', + 'Payments', + 'Admin', + 'Trading information', + 'Write', + '28/07/2023', + ]; + }); + + it('should render ApiTokenTable', () => { + expectedTexts.push('Last used'); + render( + + + + ); + expect(screen.getByText('Token 1')).not.toHaveClass('da-api-token__scope-item--name'); + expectedTexts.forEach(text => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + }); + + it('should render in mobile view', () => { + expectedTexts.push('Last Used'); + (isMobile as jest.Mock).mockImplementationOnce(() => true); + render( + + + + ); + expect(screen.getByText('Token 1')).toHaveClass('da-api-token__scope-item--name'); + expectedTexts.forEach(text => { + expect(screen.getByText(text)).toBeInTheDocument(); + }); + }); + + it('should display Never if last_used is undefined', () => { + mock_props.api_tokens = [ + { + display_name: 'Token 1', + token: 'token_1', + scopes: ['read', 'trade', 'payments', 'admin', 'trading_information', 'write'], + last_used: '', + }, + ]; + render( + + + + ); + expect(screen.getByText('Never')).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Components/api-token/api-token-article.tsx b/packages/account/src/Components/api-token/api-token-article.tsx index 577d494e76f7..5a53dee863ca 100644 --- a/packages/account/src/Components/api-token/api-token-article.tsx +++ b/packages/account/src/Components/api-token/api-token-article.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { localize, Localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; import AccountArticle from 'Components/article'; const ApiTokenArticle = () => ( } descriptions={[ + - - + ); }; diff --git a/packages/account/src/Components/api-token/api-token-context.ts b/packages/account/src/Components/api-token/api-token-context.ts index 9b147abf9291..7bb692721c14 100644 --- a/packages/account/src/Components/api-token/api-token-context.ts +++ b/packages/account/src/Components/api-token/api-token-context.ts @@ -1,6 +1,10 @@ import * as React from 'react'; import { TApiContext } from 'Types'; -const ApiTokenContext = React.createContext({}); +const ApiTokenContext = React.createContext({ + api_tokens: [], + deleteToken: () => Promise.resolve(), + isSuccess: false, +}); export default ApiTokenContext; diff --git a/packages/account/src/Components/api-token/api-token-delete-button.tsx b/packages/account/src/Components/api-token/api-token-delete-button.tsx index 24b1b9fc64a1..7328d8f452dd 100644 --- a/packages/account/src/Components/api-token/api-token-delete-button.tsx +++ b/packages/account/src/Components/api-token/api-token-delete-button.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Button, Icon, Modal, Text, Popover } from '@deriv/components'; import { isDesktop, useIsMounted } from '@deriv/shared'; -import { localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; import ApiTokenContext from './api-token-context'; import { TPopoverAlignment, TToken, TApiContext } from 'Types'; @@ -11,7 +11,7 @@ type TApiTokenDeleteButton = { }; const ApiTokenDeleteButton = ({ token, popover_alignment = 'left' }: TApiTokenDeleteButton) => { - const { deleteToken } = React.useContext(ApiTokenContext); + const { deleteToken, isSuccess } = React.useContext(ApiTokenContext); const [is_deleting, setIsDeleting] = React.useState(false); const [is_loading, setIsLoading] = React.useState(false); const [is_popover_open, setIsPopoverOpen] = React.useState(false); @@ -34,12 +34,11 @@ const ApiTokenDeleteButton = ({ token, popover_alignment = 'left' }: TApiTokenDe const handleYes = () => { setIsLoading(true); - deleteToken(token.token).finally(() => { - if (isMounted()) { - setIsLoading(false); - setIsDeleting(false); - } - }); + deleteToken(token.token); + if (isMounted() && isSuccess) { + setIsLoading(false); + setIsDeleting(false); + } }; return ( @@ -47,36 +46,32 @@ const ApiTokenDeleteButton = ({ token, popover_alignment = 'left' }: TApiTokenDe - {localize('Delete token')} + - {localize('Are you sure you want to delete this token?')} + + } relative_render={false} zIndex='9999' is_open={is_popover_open} diff --git a/packages/account/src/Components/api-token/api-token-table-row-header.tsx b/packages/account/src/Components/api-token/api-token-table-row-header.tsx index d9948862ec33..b4f9762299b0 100644 --- a/packages/account/src/Components/api-token/api-token-table-row-header.tsx +++ b/packages/account/src/Components/api-token/api-token-table-row-header.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Text } from '@deriv/components'; type TApiTokenTableRowHeader = { - text: string; + text: JSX.Element; }; const ApiTokenTableRowHeader = ({ text }: TApiTokenTableRowHeader) => ( diff --git a/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx b/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx index 2a80c98ddc93..38d3d3730016 100644 --- a/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx +++ b/packages/account/src/Components/api-token/api-token-table-row-token-cell.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Icon, Text, Popover } from '@deriv/components'; -import { localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; import ApiTokenClipboard from './api-token-clipboard'; type TApiTokenTableRowTokenCell = { @@ -9,7 +9,7 @@ type TApiTokenTableRowTokenCell = { }; const HiddenPasswordDots = () => ( -
+
{[...Array(15)].map((el, index) => (
))} @@ -33,15 +33,21 @@ const ApiTokenTableRowTokenCell = ({ token, scopes }: TApiTokenTableRowTokenCell )} } + success_message={} text_copy={token} scopes={scopes} /> + ) : ( + + ) + } > { - - - - + } /> + } /> + } /> + } /> diff --git a/packages/account/src/Components/api-token/api-token.tsx b/packages/account/src/Components/api-token/api-token.tsx deleted file mode 100644 index 5989b2e80513..000000000000 --- a/packages/account/src/Components/api-token/api-token.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import React from 'react'; -import classNames from 'classnames'; -import { Formik, Form, Field, FormikErrors, FieldProps, FormikHelpers } from 'formik'; -import { useSetApiToken, useGetApiToken } from '@deriv/api'; -import { APITokenResponse, ApiToken as TApitoken } from '@deriv/api-types'; -import { Timeline, Input, Button, ThemedScrollbars, Loading } from '@deriv/components'; -import { getPropertyValue } from '@deriv/shared'; -import { observer, useStore } from '@deriv/stores'; -import { Localize, localize } from '@deriv/translations'; -import { TToken } from 'Types'; -import LoadErrorMessage from 'Components/load-error-message'; -import { getApiTokenCardDetails } from 'Constants/api-token-card-details'; -import ApiTokenArticle from './api-token-article'; -import ApiTokenCard from './api-token-card'; -import ApiTokenTable from './api-token-table'; -import ApiTokenContext from './api-token-context'; -import InlineNoteWithIcon from '../inline-note-with-icon'; - -const MIN_TOKEN = 2; -const MAX_TOKEN = 32; - -type AptTokenState = { - api_tokens: NonNullable; - error_message: string; -}; - -type TApiTokenForm = { - token_name: string; - read: boolean; - trade: boolean; - payments: boolean; - trading_information: boolean; - admin: boolean; -}; - -const ApiToken = () => { - const { client, ui } = useStore(); - const { is_switching } = client; - const { is_desktop, is_mobile } = ui; - - const { updated_api_token_data, update, isSuccess, isLoading } = useSetApiToken(); - const { api_token_data, isSuccess: is_api_token_data_fetched } = useGetApiToken(); - - const [state, setState] = React.useReducer( - (prev_state: Partial, value: Partial) => ({ - ...prev_state, - ...value, - }), - { - api_tokens: [], - error_message: '', - } - ); - - const populateTokenResponse = React.useCallback((response: APITokenResponse) => { - if (response.error) { - setState({ - error_message: getPropertyValue(response, ['error', 'message']), - }); - } else { - setState({ - api_tokens: getPropertyValue(response, ['api_token', 'tokens']), - }); - } - }, []); - - React.useEffect(() => { - if (is_api_token_data_fetched) { - populateTokenResponse(api_token_data as APITokenResponse); - } - }, [api_token_data, is_api_token_data_fetched, populateTokenResponse]); - - React.useEffect(() => { - if (isSuccess) { - populateTokenResponse(updated_api_token_data as APITokenResponse); - } - }, [isSuccess, populateTokenResponse, updated_api_token_data]); - - const initial_form = { - token_name: '', - read: false, - trade: false, - payments: false, - trading_information: false, - admin: false, - }; - - const validateFields = (values: TApiTokenForm) => { - const errors: FormikErrors = {}; - const token_name = values.token_name && values.token_name.trim(); - - if (!token_name) { - errors.token_name = localize('Please enter a token name.'); - } else if (!/^[A-Za-z0-9\s_]+$/g.test(token_name)) { - errors.token_name = localize('Only letters, numbers, and underscores are allowed.'); - } else if (token_name.length < MIN_TOKEN) { - errors.token_name = localize( - 'Length of token name must be between {{MIN_TOKEN}} and {{MAX_TOKEN}} characters.', - { - MIN_TOKEN, - MAX_TOKEN, - } - ); - } else if (token_name.length > MAX_TOKEN) { - errors.token_name = localize('Maximum {{MAX_TOKEN}} characters.', { MAX_TOKEN }); - } - - return errors; - }; - - const selectedTokenScope = (values: TApiTokenForm) => - Object.keys(values).filter( - item => item !== 'token_name' && Boolean(values[item as keyof TApiTokenForm]) - ) as NonNullable[0]['scopes']>; - - const handleSubmit = (values: TApiTokenForm, { setSubmitting, resetForm }: FormikHelpers) => { - update({ - new_token: values.token_name, - new_token_scopes: selectedTokenScope(values), - }); - // if (token_response.error) { - // setFieldError('token_name', token_response.error.message); - // } else if (isMounted()) { - // setState({ - // api_tokens: getPropertyValue(token_response, ['api_token', 'tokens']), - // }); - // } - resetForm(); - setSubmitting(false); - }; - - const deleteToken = (token: string) => { - update({ delete_token: token }); - }; - - const { api_tokens, error_message } = state; - - if (is_switching) { - return ; - } - - if (error_message) { - return ; - } - - const context_value = { - api_tokens, - deleteToken, - }; - - const api_token_card_array = getApiTokenCardDetails(); - - return ( - - -
-
- - {is_mobile && } - - {({ - values, - errors, - isValid, - dirty, - touched, - handleChange, - handleBlur, - isSubmitting, - setFieldTouched, - }) => ( -
- - -
- {api_token_card_array.map(card => ( - - {card.name === 'admin' && ( - - } - title={localize('Note')} - /> - )} - - ))} -
-
- -
- - {({ field }: FieldProps) => ( - { - setFieldTouched('token_name', true); - handleChange(e); - }} - onBlur={handleBlur} - hint={localize( - 'Length of token name must be between 2 and 32 characters.' - )} - required - error={ - touched.token_name && errors.token_name - ? errors.token_name - : undefined - } - /> - )} - -
-
- - - -
- - )} -
-
- {is_desktop && } -
-
-
-
- ); -}; - -export default observer(ApiToken); diff --git a/packages/account/src/Components/api-token/index.ts b/packages/account/src/Components/api-token/index.ts index 4a5291323afa..952b536f2c7a 100644 --- a/packages/account/src/Components/api-token/index.ts +++ b/packages/account/src/Components/api-token/index.ts @@ -1,3 +1,11 @@ -import ApiToken from './api-token'; - -export default ApiToken; +export { default as ApiTokenArticle } from './api-token-article'; +export { default as ApiTokenCard } from './api-token-card'; +export { default as ApiTokenClipboard } from './api-token-clipboard'; +export { default as ApiTokenDeleteButton } from './api-token-delete-button'; +export { default as ApiTokenTableRowCell } from './api-token-table-row-cell'; +export { default as ApiTokenTableRowHeader } from './api-token-table-row-header'; +export { default as ApiTokenTableRowScopesCell } from './api-token-table-row-token-cell'; +export { default as ApiTokenRowTokenCell } from './api-token-table-row-token-cell'; +export { default as ApiTokenTableRow } from './api-token-table-row'; +export { default as ApiTokenTable } from './api-token-table'; +export { default as ApiTokenContext } from './api-token-context'; diff --git a/packages/account/src/Components/article/article.tsx b/packages/account/src/Components/article/article.tsx index 52a5484112be..82e2b011fb7f 100644 --- a/packages/account/src/Components/article/article.tsx +++ b/packages/account/src/Components/article/article.tsx @@ -10,7 +10,7 @@ type TDescriptionsItem = { }; export type TArticle = { - title: string; + title: string | JSX.Element; descriptions: Array; onClickLearnMore?: () => void; className?: string; diff --git a/packages/account/src/Components/inline-note-with-icon/inline-note-with-icon.tsx b/packages/account/src/Components/inline-note-with-icon/inline-note-with-icon.tsx index 5e772cd3d46c..41d647a1a826 100644 --- a/packages/account/src/Components/inline-note-with-icon/inline-note-with-icon.tsx +++ b/packages/account/src/Components/inline-note-with-icon/inline-note-with-icon.tsx @@ -5,7 +5,7 @@ type TInlineNoteWithIconExtend = { icon?: string; font_size?: string; message: React.ReactNode; - title?: string; + title?: string | JSX.Element; }; const InlineNoteWithIcon = ({ icon, message, font_size = 'xxxs', title }: TInlineNoteWithIconExtend) => { diff --git a/packages/account/src/Constants/api-token-card-details.tsx b/packages/account/src/Constants/api-token-card-details.tsx index 9beade4bc6bf..a7eccf094143 100644 --- a/packages/account/src/Constants/api-token-card-details.tsx +++ b/packages/account/src/Constants/api-token-card-details.tsx @@ -38,3 +38,8 @@ export const getApiTokenCardDetails = () => [ ), }, ]; + +export const TOKEN_LIMITS = { + MIN: 2, + MAX: 32, +}; diff --git a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx b/packages/account/src/Sections/Security/ApiToken/__tests__/api-token.spec.tsx similarity index 73% rename from packages/account/src/Components/api-token/_tests_/api-token.spec.tsx rename to packages/account/src/Sections/Security/ApiToken/__tests__/api-token.spec.tsx index 9b38a1128dff..23cafcc8f9f3 100644 --- a/packages/account/src/Components/api-token/_tests_/api-token.spec.tsx +++ b/packages/account/src/Sections/Security/ApiToken/__tests__/api-token.spec.tsx @@ -1,47 +1,64 @@ import React from 'react'; import { FormikValues } from 'formik'; -import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; -import { isMobile, getPropertyValue, useIsMounted, WS } from '@deriv/shared'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { APIProvider, useApiToken } from '@deriv/api'; +import { APITokenResponse } from '@deriv/api-types'; +import { isMobile, getPropertyValue } from '@deriv/shared'; import { mockStore, StoreProvider } from '@deriv/stores'; import ApiToken from '../api-token'; +const mock_token = 'ABCDefgh1234567890'; +const mockSend = jest.fn(); + +const api_data: Partial = { + api_token: { + tokens: [ + { + display_name: 'Created by script', + last_used: '', + scopes: ['read', 'trade', 'payments', 'admin'], + token: mock_token, + valid_for_ip: '', + }, + ], + }, +}; + +jest.mock('@deriv/components', () => ({ + ...jest.requireActual('@deriv/components'), + Loading: () =>
Loading
, +})); + jest.mock('@deriv/shared', () => ({ ...jest.requireActual('@deriv/shared'), getPropertyValue: jest.fn(() => []), isMobile: jest.fn(() => false), - useIsMounted: jest.fn().mockImplementation(() => () => true), - WS: { - apiToken: jest.fn(() => - Promise.resolve({ - api_token: { - tokens: [], - }, - }) - ), - authorized: { - apiToken: jest.fn(() => - Promise.resolve({ - api_token: { - tokens: [], - }, - }) - ), - }, - }, })); -jest.mock('@deriv/components', () => ({ - ...jest.requireActual('@deriv/components'), - Loading: () =>
Loading
, +jest.mock('@deriv/api', () => ({ + ...jest.requireActual('@deriv/api'), + useApiToken: jest.fn(), })); const modal_root_el = document.createElement('div'); - beforeAll(() => { modal_root_el.setAttribute('id', 'modal_root'); document.body.appendChild(modal_root_el); }); +beforeEach(() => { + useApiToken.mockReturnValue({ + api_token_data: { ...api_data }, + isSuccess: true, + isError: false, + isLoading: false, + getApiToken: mockSend, + createApiToken: mockSend, + deleteApiToken: mockSend, + }); +}); + afterAll(() => { document.body.removeChild(modal_root_el); }); @@ -65,7 +82,7 @@ describe('', () => { const your_access_description = "To access your mobile apps and other third-party apps, you'll first need to generate an API token."; - const store = mockStore({ + const mock_store = mockStore({ client: { is_switching: false, }, @@ -75,14 +92,17 @@ describe('', () => { }, }); - it('should render ApiToken component without app_settings and footer', async () => { + const renderComponent = ({ store = mock_store }) => render( - + + + ); - expect(WS.authorized.apiToken).toHaveBeenCalled(); + it('should render ApiToken component without app_settings and footer', async () => { + renderComponent({}); expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); @@ -94,16 +114,14 @@ describe('', () => { expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); + + expect(mockSend).toHaveBeenCalledTimes(1); }); it('should render ApiToken component without app_settings and footer for mobile', async () => { (isMobile as jest.Mock).mockReturnValueOnce(true); - render( - - - - ); + renderComponent({}); expect(await screen.findByText(admin_scope_description)).toBeInTheDocument(); expect(await screen.findByText(admin_scope_note)).toBeInTheDocument(); @@ -115,39 +133,40 @@ describe('', () => { expect(await screen.findByText(trading_info_description)).toBeInTheDocument(); expect(await screen.findByText(read_scope_description)).toBeInTheDocument(); expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); + expect(useApiToken().isSuccess).toBeTruthy(); }); - it('should not render ApiToken component if is not mounted', () => { - useIsMounted(); - - render( - - - - ); - - expect(WS.authorized.apiToken).toHaveBeenCalled(); - expect(screen.getByText('Loading')).toBeInTheDocument(); + it('should not render ApiToken component if data is still loading', async () => { + const new_store = mockStore({ + client: { + is_switching: true, + }, + ui: { + is_desktop: false, + is_mobile: true, + }, + }); + renderComponent({ store: new_store }); - expect(screen.queryByText(admin_scope_description)).not.toBeInTheDocument(); - expect(screen.queryByText(admin_scope_note)).not.toBeInTheDocument(); - expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); - expect(screen.queryByText(trading_info_scope_description)).not.toBeInTheDocument(); - expect(screen.queryByText(select_scopes_msg)).not.toBeInTheDocument(); - expect(screen.queryByText(token_creation_description)).not.toBeInTheDocument(); - expect(screen.queryByText(token_using_description)).not.toBeInTheDocument(); - expect(screen.queryByText(trade_scope_description)).not.toBeInTheDocument(); - expect(screen.queryByText(trading_info_description)).not.toBeInTheDocument(); - expect(screen.queryByText(your_access_description)).not.toBeInTheDocument(); - expect(screen.queryByText(read_scope_description)).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('Loading')).toBeInTheDocument(); + + expect(screen.queryByText(admin_scope_description)).not.toBeInTheDocument(); + expect(screen.queryByText(admin_scope_note)).not.toBeInTheDocument(); + expect(screen.queryByText(learn_more_title)).not.toBeInTheDocument(); + expect(screen.queryByText(trading_info_scope_description)).not.toBeInTheDocument(); + expect(screen.queryByText(select_scopes_msg)).not.toBeInTheDocument(); + expect(screen.queryByText(token_creation_description)).not.toBeInTheDocument(); + expect(screen.queryByText(token_using_description)).not.toBeInTheDocument(); + expect(screen.queryByText(trade_scope_description)).not.toBeInTheDocument(); + expect(screen.queryByText(trading_info_description)).not.toBeInTheDocument(); + expect(screen.queryByText(your_access_description)).not.toBeInTheDocument(); + expect(screen.queryByText(read_scope_description)).not.toBeInTheDocument(); + }); }); it('should choose checkbox, enter a valid value and create token', async () => { - render( - - - - ); + renderComponent({}); expect(screen.queryByText('New token name')).not.toBeInTheDocument(); @@ -161,35 +180,35 @@ describe('', () => { expect(read_checkbox?.checked).toBeFalsy(); expect(token_name_input?.value).toBe(''); - fireEvent.click(read_checkbox); + userEvent.click(read_checkbox); expect(read_checkbox?.checked).toBeTruthy(); - fireEvent.change(token_name_input, { target: { value: '@#$' } }); + userEvent.type(token_name_input, '@#$'); expect(await screen.findByText('Only letters, numbers, and underscores are allowed.')).toBeInTheDocument(); + userEvent.clear(token_name_input); - fireEvent.change(token_name_input, { target: { value: 'N' } }); + userEvent.type(token_name_input, 'N'); expect(await screen.findByText(/length of token name must be between/i)).toBeInTheDocument(); + userEvent.clear(token_name_input); - fireEvent.change(token_name_input, { target: { value: 'New test extra long name for erorr' } }); + userEvent.type(token_name_input, 'New test extra long name for erorr'); expect(await screen.findByText(/maximum/i)).toBeInTheDocument(); + userEvent.clear(token_name_input); - fireEvent.change(token_name_input, { target: { value: 'New token name' } }); + userEvent.type(token_name_input, 'New token name'); await waitFor(() => { expect(token_name_input.value).toBe('New token name'); }); expect(create_btn).toBeEnabled(); - fireEvent.click(create_btn); + userEvent.click(create_btn); const updated_token_name_input = (await screen.findByLabelText('Token name')) as HTMLInputElement; expect(updated_token_name_input.value).toBe(''); - const createToken = WS.apiToken; - expect(createToken).toHaveBeenCalledTimes(1); + expect(mockSend).toHaveBeenCalled(); }); it('should render created tokens and trigger delete', async () => { - jest.useFakeTimers(); - (getPropertyValue as jest.Mock).mockReturnValue([ { display_name: 'First test token', @@ -207,11 +226,7 @@ describe('', () => { }, ]); - render( - - - - ); + renderComponent({}); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(await screen.findByText('Last used')).toBeInTheDocument(); @@ -223,11 +238,11 @@ describe('', () => { const delete_btns_1 = screen.getAllByTestId('dt_token_delete_icon'); expect(delete_btns_1).toHaveLength(2); - fireEvent.click(delete_btns_1[0]); + userEvent.click(delete_btns_1[0]); const no_btn_1 = screen.getByRole('button', { name: /cancel/i }); expect(no_btn_1).toBeInTheDocument(); - fireEvent.click(no_btn_1); + userEvent.click(no_btn_1); await waitFor(() => { expect(no_btn_1).not.toBeInTheDocument(); }); @@ -235,21 +250,18 @@ describe('', () => { const delete_btns_2 = await screen.findAllByTestId('dt_token_delete_icon'); expect(delete_btns_2).toHaveLength(2); - fireEvent.click(delete_btns_2[0]); + userEvent.click(delete_btns_2[0]); const yes_btn_1 = screen.getByRole('button', { name: /yes, delete/i }); expect(yes_btn_1).toBeInTheDocument(); - fireEvent.click(yes_btn_1); - const deleteToken = WS.authorized.apiToken; - expect(deleteToken).toHaveBeenCalled(); + userEvent.click(yes_btn_1); + expect(mockSend).toHaveBeenCalled(); await waitFor(() => { expect(yes_btn_1).not.toBeInTheDocument(); }); }); it('should trigger hide/unhide icon and trigger copy icon, should show dialog only for admin scope', async () => { - jest.useFakeTimers(); - const warning_msg = 'Be careful who you share this token with. Anyone with this token can perform the following actions on your account behalf'; @@ -270,11 +282,7 @@ describe('', () => { }, ]); - render( - - - - ); + renderComponent({}); expect(await screen.findByText('First test token')).toBeInTheDocument(); expect(screen.queryByText('FirstTokenID')).not.toBeInTheDocument(); @@ -282,30 +290,26 @@ describe('', () => { const toggle_visibility_btns = await screen.findAllByTestId('dt_toggle_visibility_icon'); expect(toggle_visibility_btns).toHaveLength(2); - fireEvent.click(toggle_visibility_btns[0]); + userEvent.click(toggle_visibility_btns[0]); expect(screen.getByText('FirstTokenID')).toBeInTheDocument(); - fireEvent.click(toggle_visibility_btns[1]); + userEvent.click(toggle_visibility_btns[1]); expect(screen.getByText('SecondTokenID')).toBeInTheDocument(); const copy_btns_1 = await screen.findAllByTestId('dt_copy_token_icon'); expect(copy_btns_1).toHaveLength(2); - fireEvent.click(copy_btns_1[0]); + userEvent.click(copy_btns_1[0]); expect(screen.queryByText(warning_msg)).not.toBeInTheDocument(); - - act(() => { - jest.advanceTimersByTime(2100); - }); expect(screen.queryByTestId('dt_token_copied_icon')).not.toBeInTheDocument(); - fireEvent.click(copy_btns_1[1]); + userEvent.click(copy_btns_1[1]); expect(await screen.findByText(warning_msg)).toBeInTheDocument(); const ok_btn = screen.getByRole('button', { name: /ok/i }); expect(ok_btn).toBeInTheDocument(); - fireEvent.click(ok_btn); + userEvent.click(ok_btn); jest.clearAllMocks(); }); @@ -336,11 +340,7 @@ describe('', () => { }, ]); - render( - - - - ); + renderComponent({}); expect(await screen.findAllByText('Name')).toHaveLength(3); expect(await screen.findAllByText('Last Used')).toHaveLength(3); @@ -355,20 +355,19 @@ describe('', () => { }); it('should show token error if exists', async () => { - WS.authorized.apiToken = jest.fn(() => - Promise.resolve({ - api_token: { tokens: [] }, - error: { message: 'New test error' }, - }) - ); + useApiToken.mockReturnValue({ + isSuccess: false, + isError: true, + isLoading: false, + error: { message: 'New test error' }, + getApiToken: mockSend, + createApiToken: mockSend, + deleteApiToken: mockSend, + }); (getPropertyValue as jest.Mock).mockReturnValue('New test error'); - render( - - - - ); + renderComponent({}); expect(await screen.findByText('New test error')).toBeInTheDocument(); }); diff --git a/packages/account/src/Components/api-token/api-token.scss b/packages/account/src/Sections/Security/ApiToken/api-token.scss similarity index 100% rename from packages/account/src/Components/api-token/api-token.scss rename to packages/account/src/Sections/Security/ApiToken/api-token.scss diff --git a/packages/account/src/Sections/Security/ApiToken/api-token.tsx b/packages/account/src/Sections/Security/ApiToken/api-token.tsx index cb02ee645088..50ed8c6794f7 100644 --- a/packages/account/src/Sections/Security/ApiToken/api-token.tsx +++ b/packages/account/src/Sections/Security/ApiToken/api-token.tsx @@ -1,4 +1,264 @@ -import ApiToken from 'Components/api-token/api-token'; -import 'Components/api-token/api-token.scss'; +import React from 'react'; +import classNames from 'classnames'; +import { Formik, Form, Field, FormikErrors, FieldProps, FormikHelpers } from 'formik'; +import { useApiToken } from '@deriv/api'; +import { ApiToken as TApitoken } from '@deriv/api-types'; +import { Timeline, Input, Button, ThemedScrollbars, Loading } from '@deriv/components'; +import { getPropertyValue } from '@deriv/shared'; +import { observer, useStore } from '@deriv/stores'; +import { Localize, localize } from '@deriv/translations'; +import { TToken } from 'Types'; +import { ApiTokenContext, ApiTokenArticle, ApiTokenCard, ApiTokenTable } from 'Components/api-token'; +import InlineNoteWithIcon from 'Components/inline-note-with-icon'; +import LoadErrorMessage from 'Components/load-error-message'; +import { getApiTokenCardDetails, TOKEN_LIMITS } from 'Constants/api-token-card-details'; +import './api-token.scss'; -export default ApiToken; +type AptTokenState = { + api_tokens: NonNullable; + error_message: string; +}; + +type TApiTokenForm = { + token_name: string; + read: boolean; + trade: boolean; + payments: boolean; + trading_information: boolean; + admin: boolean; +}; + +const ApiToken = () => { + const { client, ui } = useStore(); + const { is_switching } = client; + const { is_desktop, is_mobile } = ui; + + const { api_token_data, getApiToken, createApiToken, deleteApiToken, isSuccess, isLoading, isError, error } = + useApiToken(); + + const [state, setState] = React.useReducer( + (prev_state: Partial, value: Partial) => ({ + ...prev_state, + ...value, + }), + { + api_tokens: [], + error_message: '', + } + ); + + React.useEffect(() => { + /** + * Fetch all API tokens + */ + getApiToken(); + }, [getApiToken]); + + React.useEffect(() => { + /** + * Update API token list when new token is created or a token is deleted + */ + if (isSuccess) { + setState({ + api_tokens: getPropertyValue(api_token_data, ['tokens']), + }); + } else if (isError) { + setState({ + error_message: getPropertyValue(error, ['message']), + }); + } + }, [isSuccess, api_token_data, isError, error]); + + const initial_form: TApiTokenForm = { + token_name: '', + read: false, + trade: false, + payments: false, + trading_information: false, + admin: false, + }; + + const validateFields = (values: TApiTokenForm) => { + const errors: FormikErrors = {}; + const token_name = values.token_name && values.token_name.trim(); + + if (!token_name) { + errors.token_name = localize('Please enter a token name.'); + } else if (!/^[A-Za-z0-9\s_]+$/g.test(token_name)) { + errors.token_name = localize('Only letters, numbers, and underscores are allowed.'); + } else if (token_name.length < TOKEN_LIMITS.MIN) { + errors.token_name = localize( + 'Length of token name must be between {{MIN_TOKEN}} and {{MAX_TOKEN}} characters.', + { + MIN_TOKEN: TOKEN_LIMITS.MIN, + MAX_TOKEN: TOKEN_LIMITS.MAX, + } + ); + } else if (token_name.length > TOKEN_LIMITS.MAX) { + errors.token_name = localize('Maximum {{MAX_TOKEN}} characters.', { MAX_TOKEN: TOKEN_LIMITS.MAX }); + } + + return errors; + }; + + const selectedTokenScope = (values: TApiTokenForm) => + Object.keys(values).filter( + item => item !== 'token_name' && Boolean(values[item as keyof TApiTokenForm]) + ) as NonNullable[0]['scopes']>; + + const handleSubmit = (values: TApiTokenForm, { setSubmitting, resetForm }: FormikHelpers) => { + createApiToken({ + new_token: values.token_name, + new_token_scopes: selectedTokenScope(values), + }); + resetForm(); + setSubmitting(false); + }; + + const deleteToken = (token: string) => { + deleteApiToken(token); + }; + + const { api_tokens, error_message } = state; + + if (is_switching) { + return ; + } + + if (error_message) { + return ; + } + + const context_value = { + api_tokens, + deleteToken, + isSuccess, + }; + + const api_token_card_array = getApiTokenCardDetails(); + + return ( + + +
+
+ + {is_mobile && } + + {({ + values, + errors, + isValid, + dirty, + touched, + handleChange, + handleBlur, + isSubmitting, + setFieldTouched, + }) => ( +
+ + + } + > +
+ {api_token_card_array.map(card => ( + + {card.name === 'admin' && ( + + } + title={} + /> + )} + + ))} +
+
+ + } + > +
+ + {({ field }: FieldProps) => ( + } + value={values.token_name} + onChange={e => { + setFieldTouched('token_name', true); + handleChange(e); + }} + onBlur={handleBlur} + hint={ + + } + required + error={ + touched.token_name && errors.token_name + ? errors.token_name + : undefined + } + /> + )} + + +
+
+ + } + > + + +
+ + )} +
+
+ {is_desktop && } +
+
+
+
+ ); +}; + +export default observer(ApiToken); diff --git a/packages/account/src/Types/context.type.ts b/packages/account/src/Types/context.type.ts index 714aa7e1d0e5..cf9643605332 100644 --- a/packages/account/src/Types/context.type.ts +++ b/packages/account/src/Types/context.type.ts @@ -3,4 +3,5 @@ import { TToken } from './common-prop.type'; export type TApiContext = { api_tokens: NonNullable; deleteToken: (token: string) => Promise; + isSuccess: boolean; }; diff --git a/packages/api/src/hooks/__tests__/useApiToken.spec.tsx b/packages/api/src/hooks/__tests__/useApiToken.spec.tsx new file mode 100644 index 000000000000..1725f2830b23 --- /dev/null +++ b/packages/api/src/hooks/__tests__/useApiToken.spec.tsx @@ -0,0 +1,58 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useApiToken from '../useApiToken'; +import APIProvider from '../../APIProvider'; +import React from 'react'; +import { WS } from '@deriv/shared'; + +jest.mock('@deriv/shared', () => ({ + WS: { + send: jest.fn().mockResolvedValueOnce({ + msg_type: 'api_token', + echo_req: {}, + api_token: { + tokens: [ + { + display_name: 'Created by script', + last_used: '', + scopes: ['read', 'trade', 'payments', 'admin'], + token: '', + valid_for_ip: '', + }, + { + display_name: 'test12', + last_used: '', + scopes: ['read', 'payments'], + token: '', + valid_for_ip: '', + }, + ], + }, + }), + }, +})); + +describe('useApiToken', () => { + it('should return the token data when a get call is made', async () => { + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + const { result, waitForNextUpdate } = renderHook(() => useApiToken(), { wrapper }); + + result.current.getApiToken(); + + await waitForNextUpdate(); + + expect(result.current.api_token_data?.tokens).toHaveLength(2); + }); + + it('should return error when error is thrown', async () => { + const error_message = { message: 'Invalid API token' }; + WS.send.mockResolvedValueOnce({ error: error_message }); + const wrapper = ({ children }: { children: JSX.Element }) => {children}; + const { result, waitForNextUpdate } = renderHook(() => useApiToken(), { wrapper }); + + result.current.getApiToken(); + + await waitForNextUpdate(); + + expect(result.current.error).toMatchObject(error_message); + }); +}); diff --git a/packages/api/src/hooks/index.ts b/packages/api/src/hooks/index.ts index 8352cddccb29..059da047168c 100644 --- a/packages/api/src/hooks/index.ts +++ b/packages/api/src/hooks/index.ts @@ -17,7 +17,6 @@ export { default as useTradingAccountsList } from './useTradingAccountsList'; export { default as useTradingPlatformAccounts } from './useTradingPlatformAccounts'; export { default as useTradingPlatformAvailableAccounts } from './useTradingPlatformAvailableAccounts'; export { default as useWalletAccountsList } from './useWalletAccountsList'; -export { default as useGetApiToken } from './useGetApiToken'; -export { default as useSetApiToken } from './useSetApiToken'; export { default as useCreateMT5Account } from './useCreateMT5Account'; export { default as useCreateOtherCFDAccount } from './useCreateOtherCFDAccount'; +export { default as useApiToken } from './useApiToken'; diff --git a/packages/api/src/hooks/useApiToken.ts b/packages/api/src/hooks/useApiToken.ts new file mode 100644 index 000000000000..57187c3691ca --- /dev/null +++ b/packages/api/src/hooks/useApiToken.ts @@ -0,0 +1,55 @@ +import React from 'react'; +import useRequest from '../useRequest'; +import useInvalidateQuery from '../useInvalidateQuery'; + +type TAPITokenPayload = NonNullable< + NonNullable>['mutate']>>[0]>['payload'] +>; + +type TDeleteAPITokenPayload = NonNullable['delete_token']; +type TCreateAPITokenPayload = Omit, 'delete_token'>; + +/** + * Makes an API call to GET, UPDATE or DELETE API token. + * @name useApiToken + * @returns an object containing the API token data, update function and status of the request/response. + */ +const useApiToken = () => { + const invalidate = useInvalidateQuery(); + const { data, mutate, ...rest } = useRequest('api_token', { + onSuccess: () => invalidate('api_token'), + }); + + /** + * Makes an API call to GET API token. + * @name getApiToken + */ + const getApiToken = React.useCallback(() => mutate({}), [mutate]); + + /** + * Makes an API call to CREATE API token. + * @name createApiToken + * @param payload - The name and scope of the API token. + */ + const createApiToken = React.useCallback((payload: TCreateAPITokenPayload) => mutate({ payload }), [mutate]); + + /** + * Makes an API call to DELETE API token. + * @name deleteApiToken + * @param value - The name of the API token. + */ + const deleteApiToken = React.useCallback( + (value: TDeleteAPITokenPayload) => mutate({ payload: { delete_token: value } }), + [mutate] + ); + + return { + api_token_data: data?.api_token, + getApiToken, + createApiToken, + deleteApiToken, + ...rest, + }; +}; + +export default useApiToken; diff --git a/packages/components/src/components/popup/popup-overlay.tsx b/packages/components/src/components/popup/popup-overlay.tsx index 4f922c768f95..92a6a879fe29 100644 --- a/packages/components/src/components/popup/popup-overlay.tsx +++ b/packages/components/src/components/popup/popup-overlay.tsx @@ -4,7 +4,7 @@ import Text from '../text/text'; import Button from '../button/button'; type TPopupOverlay = { - title: string; + title: string | JSX.Element; descriptions: { key: number | string; component: React.ReactNode; From b8b7324d45cc2c48ddf3e572d5425921f2ccce89 Mon Sep 17 00:00:00 2001 From: Likhith Kolayari Date: Mon, 11 Sep 2023 12:08:17 +0400 Subject: [PATCH 19/22] fix: prop type --- packages/account/src/Types/common-prop.type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/account/src/Types/common-prop.type.ts b/packages/account/src/Types/common-prop.type.ts index 10073de1f117..23b8cfa7623f 100644 --- a/packages/account/src/Types/common-prop.type.ts +++ b/packages/account/src/Types/common-prop.type.ts @@ -191,7 +191,7 @@ export type TVerificationStatus = Readonly< >; export type TIDVFormValues = { - document_type: TDocumentList[0]; + document_type: TDocumentList; document_number: string; document_additional?: string; error_message?: string; From 0ff5b3743ddfb701015603ce94db740e991a24fd Mon Sep 17 00:00:00 2001 From: Likhith Kolayari Date: Mon, 11 Sep 2023 13:10:32 +0400 Subject: [PATCH 20/22] fix: :white_check_mark: fixed testcase --- packages/account/src/Sections/Security/ApiToken/api-token.tsx | 2 +- packages/components/src/components/input/input.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/account/src/Sections/Security/ApiToken/api-token.tsx b/packages/account/src/Sections/Security/ApiToken/api-token.tsx index 50ed8c6794f7..e8b888e84972 100644 --- a/packages/account/src/Sections/Security/ApiToken/api-token.tsx +++ b/packages/account/src/Sections/Security/ApiToken/api-token.tsx @@ -197,7 +197,7 @@ const ApiToken = () => { data-lpignore='true' type='text' className='da-api-token__input dc-input__input-group' - label={} + label={localize('Token name')} value={values.token_name} onChange={e => { setFieldTouched('token_name', true); diff --git a/packages/components/src/components/input/input.tsx b/packages/components/src/components/input/input.tsx index 31e24c7026a3..5393f7999c83 100644 --- a/packages/components/src/components/input/input.tsx +++ b/packages/components/src/components/input/input.tsx @@ -21,7 +21,7 @@ export type TInputProps = { input_id?: string; is_relative_hint?: boolean; label_className?: string; - label?: React.ReactNode; + label?: string; leading_icon?: React.ReactElement; max_characters?: number; maxLength?: number; From f81c60b9f4a6cc09cfde4c466345f129b01b6a7e Mon Sep 17 00:00:00 2001 From: Likhith Kolayari Date: Mon, 11 Sep 2023 13:22:53 +0400 Subject: [PATCH 21/22] feat: :art: migrated to Localize --- .../Components/api-token/api-token-clipboard.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/account/src/Components/api-token/api-token-clipboard.tsx b/packages/account/src/Components/api-token/api-token-clipboard.tsx index 7b88b4010dd7..5be792dcb48e 100644 --- a/packages/account/src/Components/api-token/api-token-clipboard.tsx +++ b/packages/account/src/Components/api-token/api-token-clipboard.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useIsMounted } from '@deriv/shared'; import { Button, Icon, Modal, Text, Popover, useCopyToClipboard } from '@deriv/components'; -import { Localize, localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; import { TPopoverAlignment } from 'Types'; type TApiTokenClipboard = { @@ -13,7 +13,7 @@ type TApiTokenClipboard = { }; type TWarningNoteBullet = { - message: string; + message: string | JSX.Element; }; const WarningNoteBullet = ({ message }: TWarningNoteBullet) => ( @@ -28,14 +28,14 @@ const WarningNoteBullet = ({ message }: TWarningNoteBullet) => ( const WarningDialogMessage = () => ( - {localize( - 'Be careful who you share this token with. Anyone with this token can perform the following actions on your account behalf' - )} +
- - - + } /> + } + /> + } />
); From 0d9a82b36a9b0dc9aa5a634117cb99952b40c7cf Mon Sep 17 00:00:00 2001 From: utkarsha-deriv Date: Mon, 11 Sep 2023 13:35:16 +0400 Subject: [PATCH 22/22] fix: localize to Localize for Composite checkbox --- .../account/src/Constants/api-token-card-details.tsx | 12 ++++++------ .../composite-checkbox/composite-checkbox.tsx | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/account/src/Constants/api-token-card-details.tsx b/packages/account/src/Constants/api-token-card-details.tsx index a7eccf094143..2a16c1b9a4e1 100644 --- a/packages/account/src/Constants/api-token-card-details.tsx +++ b/packages/account/src/Constants/api-token-card-details.tsx @@ -1,38 +1,38 @@ import React from 'react'; -import { Localize, localize } from '@deriv/translations'; +import { Localize } from '@deriv/translations'; export const getApiTokenCardDetails = () => [ { name: 'read', - display_name: localize('Read'), + display_name: , description: ( ), }, { name: 'trade', - display_name: localize('Trade'), + display_name: , description: ( ), }, { name: 'payments', - display_name: localize('Payments'), + display_name: , description: ( ), }, { name: 'trading_information', - display_name: localize('Trading information'), + display_name: , description: ( ), }, { name: 'admin', - display_name: localize('Admin'), + display_name: , description: ( ), diff --git a/packages/components/src/components/composite-checkbox/composite-checkbox.tsx b/packages/components/src/components/composite-checkbox/composite-checkbox.tsx index 31e3ab61973f..ccb6d263bef3 100644 --- a/packages/components/src/components/composite-checkbox/composite-checkbox.tsx +++ b/packages/components/src/components/composite-checkbox/composite-checkbox.tsx @@ -9,7 +9,7 @@ type TCompositeCheckbox = { onChange: React.FormEventHandler & ((e: React.ChangeEvent | React.KeyboardEvent) => void); className?: string; - label: string; + label: JSX.Element; id?: string; description: JSX.Element; };