diff --git a/packages/account/src/Components/article/__tests__/article.spec.tsx b/packages/account/src/Components/article/__tests__/article.spec.tsx index 11fbd953aa66..7691c740f816 100644 --- a/packages/account/src/Components/article/__tests__/article.spec.tsx +++ b/packages/account/src/Components/article/__tests__/article.spec.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { screen, render } from '@testing-library/react'; import AccountArticle, { TArticle } from '../article'; +import userEvent from '@testing-library/user-event'; describe('', () => { const props: TArticle = { @@ -38,4 +39,11 @@ describe('', () => { expect(screen.getByText('Description 3')).toBeInTheDocument(); expect(screen.getByText('Description 4')).toBeInTheDocument(); }); + + it("should invoke the callback on clicking the 'Learn more' link", () => { + render(); + + userEvent.click(screen.getByText(/Learn more/i)); + expect(props.onClickLearnMore).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/account/src/Components/article/article.tsx b/packages/account/src/Components/article/article.tsx index 52a5484112be..9febac3c3ddb 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: JSX.Element | string; descriptions: Array; onClickLearnMore?: () => void; className?: string; diff --git a/packages/account/src/Constants/connected-apps-config.tsx b/packages/account/src/Constants/connected-apps-config.tsx new file mode 100644 index 000000000000..18fd3e3f4b6a --- /dev/null +++ b/packages/account/src/Constants/connected-apps-config.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Localize } from '@deriv/translations'; + +export const CONNECTED_APPS_INFO_BULLETS = [ + { + key: 1, + text: ( + + ), + }, + { + key: 2, + text: ( + + ), + }, + { + key: 3, + text: ( + + ), + }, +]; diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-earn-more.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-earn-more.spec.tsx new file mode 100644 index 000000000000..b83ceddd9cd9 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-earn-more.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import ConnectedAppsEarnMore from '../connected-apps-earn-more'; + +describe('ConnectedAppsEarnMore', () => { + it("should render the 'Earn more' section with correct details", () => { + render(); + expect(screen.getByText(/Earn more with Deriv API/i)).toBeInTheDocument(); + expect( + screen.getByText( + /Use our powerful, flexible, and free API to build a custom trading platform for yourself or for your business./i + ) + ).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-empty.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-empty.spec.tsx new file mode 100644 index 000000000000..0a1cf5cd80f8 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-empty.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import ConnectedAppsEmpty from '../connected-apps-empty'; + +describe('ConnectedAppsEmpty', () => { + const renderComponent = (mock_store = mockStore({})) => + render( + + + + ); + + it('should render the empty apps informative text component with correct details', () => { + renderComponent(); + + expect( + screen.getByText(/You currently don't have any third-party authorised apps associated with your account./i) + ).toBeInTheDocument(); + expect( + screen.getByText( + /Connected apps are authorised applications associated with your account through your API token or the OAuth authorisation process. They can act on your behalf within the limitations that you have set./i + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + /As a user, you are responsible for sharing access and for actions that occur in your account \(even if they were initiated by a third-party app on your behalf\)./i + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + /Please note that only third-party apps will be displayed on this page. Official Deriv apps will not appear here./i + ) + ).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-info-bullets.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-info-bullets.spec.tsx new file mode 100644 index 000000000000..4de4742dc459 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-info-bullets.spec.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import ConnectedAppsInfoBullets from '../connected-apps-info-bullets'; + +describe('ConnectedAppsInfoBullets', () => { + it('should render the 3 informative ordered list items', () => { + render( + + {' '} + + ); + + const ordered_list = screen.getAllByRole('listitem'); + expect(ordered_list).toHaveLength(3); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-info.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-info.spec.tsx new file mode 100644 index 000000000000..fa4e2a8f1855 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-info.spec.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import ConnectedAppsInfo from '../connected-apps-info'; + +describe('', () => { + beforeEach(() => { + render( + + + + ); + }); + + it('should have h4 element with text "What are connected apps"', () => { + const heading = screen.getByRole('heading', { name: 'What are connected apps?' }); + expect(heading).toBeInTheDocument(); + }); + + it('should have an ordered list', () => { + const orderedlist = screen.getByRole('list'); + expect(orderedlist).toBeInTheDocument(); + }); + + it('should have three list items', () => { + const listitems = screen.getAllByRole('listitem'); + expect(listitems).toHaveLength(3); + }); + + it('displays connected apps information', () => { + expect( + screen.getByText( + 'Connected apps are authorised applications associated with your account through your API token or the OAuth authorisation process. They can act on your behalf within the limitations that you have set.' + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + 'As a user, you are responsible for sharing access and for actions that occur in your account (even if they were initiated by a third-party app on your behalf).' + ) + ).toBeInTheDocument(); + expect( + screen.getByText( + 'Please note that only third-party apps will be displayed on this page. Official Deriv apps will not appear here.' + ) + ).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-know-more.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-know-more.spec.tsx new file mode 100644 index 000000000000..24d945890d2d --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps-know-more.spec.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import ConnectedAppsKnowMore from '../connected-apps-know-more'; + +describe('ConnectedAppsKnowMore', () => { + it("should render the 'Know more' section with correct details", () => { + render(); + expect(screen.getByText(/Want to know more about APIs\?/i)).toBeInTheDocument(); + expect( + screen.getByText( + /Go to our Deriv community and learn about APIs, API tokens, ways to use Deriv APIs, and more./i + ) + ).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps.spec.js b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps.spec.js deleted file mode 100644 index 4d8f443310c8..000000000000 --- a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps.spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { render, screen, waitFor, waitForElementToBeRemoved, fireEvent, act } from '@testing-library/react'; -import ConnectedApps from '../connected-apps'; - -const true_oauth_apps_list = { - oauth_apps: [ - { - name: 'Local', - app_markup_percentage: 0, - app_id: 9999, - scopes: ['read', 'admin', 'trade', 'payments'], - last_used: '2021-10-31 06:49:52', - }, - ], -}; - -const empty_oauth_apps_list = { oauth_apps: [] }; - -jest.mock('@deriv/shared/src/services/ws-methods', () => ({ - __esModule: true, - default: 'mockedDefaultExport', - WS: { - authorized: { - send: ({ revoke_oauth_app }) => { - if (revoke_oauth_app) empty_oauth_apps_list; - return true_oauth_apps_list; - }, - }, - }, -})); - -describe('Connected Apps', () => { - const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight'); - const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth'); - - beforeAll(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 50 }); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 50 }); - const modal_root_el = document.createElement('div'); - modal_root_el.setAttribute('id', 'modal_root'); - document.body.appendChild(modal_root_el); - }); - - afterAll(() => { - Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight); - Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth); - let modal_root_el = document.getElementById('modal_root'); - document.body.removeChild(modal_root_el); - }); - - test('renders correctly', async () => { - const { container } = render(); - - expect(screen.getByText(/Authorised applications/i)).toBeInTheDocument(); - - await waitForElementToBeRemoved(() => container.querySelector('.initial-loader')); - - await waitFor(() => { - expect(screen.getByText('Local')).toBeInTheDocument(); - expect(screen.getByText(true_oauth_apps_list.oauth_apps[0].last_used)).toBeInTheDocument(); - expect(screen.getByText('Revoke access')).toBeInTheDocument(); - }); - }); - - test('revoke access when click on confirm', async () => { - const { container } = render(); - - await waitForElementToBeRemoved(() => container.querySelector('.initial-loader')); - fireEvent.click(screen.getByText('Revoke access')); - - await waitFor(() => { - expect(screen.getByText('Confirm')).toBeInTheDocument(); - expect(screen.getByText('Back')).toBeInTheDocument(); - }); - - act(() => { - fireEvent.click(screen.getByText('Confirm')); - }); - - await waitFor(() => { - expect(screen.queryByText('Local')).not.toBeInTheDocument(); - }); - }); -}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps.spec.tsx new file mode 100644 index 000000000000..be6b7d141218 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/connected-apps.spec.tsx @@ -0,0 +1,165 @@ +import React from 'react'; +import { OauthApps } from '@deriv/api-types'; +import { StoreProvider, mockStore } from '@deriv/stores'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ConnectedApps from '../connected-apps'; +import { WS } from '@deriv/shared'; + +const mock_connected_apps: OauthApps = [ + { + name: 'Local', + app_markup_percentage: 0, + app_id: 9999, + scopes: ['read', 'admin', 'trade', 'payments'], + last_used: '2021-10-31 06:49:52', + official: 0, + appstore: '', + github: '', + googleplay: '', + homepage: '', + redirect_uri: '', + verification_uri: '', + active: 0, + }, +]; +jest.mock('@deriv/shared', () => ({ + ...jest.requireActual('@deriv/shared'), + WS: { + authorized: { + send: jest.fn(() => ({ oauth_apps: mock_connected_apps })), + }, + }, +})); +jest.mock('@deriv/components', () => ({ + ...jest.requireActual('@deriv/components'), + Loading: jest.fn(() =>
Mocked Loading
), +})); +jest.mock('../connected-apps-earn-more', () => jest.fn(() =>
Mocked Earn More
)); +jest.mock('../connected-apps-empty', () => jest.fn(() =>
Mocked Empty Apps
)); +jest.mock('../connected-apps-know-more', () => jest.fn(() =>
Mocked Know More
)); + +describe('ConnectedApps', () => { + const originalOffsetHeight = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetHeight'); + const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth'); + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 50 }); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 50 }); + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + }); + + afterAll(() => { + Object.defineProperty(HTMLElement.prototype, 'offsetHeight', originalOffsetHeight as PropertyDescriptor); + Object.defineProperty(HTMLElement.prototype, 'offsetWidth', originalOffsetWidth as PropertyDescriptor); + document.body.removeChild(modal_root_el); + }); + + const renderComponent = (mock_store = mockStore({})) => + render( + + + + ); + + it('should render the Loading component initially', async () => { + renderComponent(); + + expect(screen.getByText(/Mocked Loading/i)).toBeInTheDocument(); + }); + + it("should render the 'Know more' component", async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByText(/Mocked Know More/i)).toBeInTheDocument(); + }); + }); + + it("should render the 'Earn more' component", async () => { + renderComponent(); + + await waitFor(() => { + expect(screen.getByText(/Mocked Earn More/i)).toBeInTheDocument(); + }); + }); + + it('should render the app list in Desktop view', async () => { + renderComponent(); + const mock_permissions = mock_connected_apps[0]?.scopes + ?.map(scope => scope.charAt(0).toUpperCase().concat(scope.substring(1))) + .join(', '); + + await waitFor(() => { + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText(mock_connected_apps[0].name)).toBeInTheDocument(); + expect(screen.getByText('Last login')).toBeInTheDocument(); + if (mock_connected_apps[0]?.last_used) { + expect(screen.getByText(mock_connected_apps[0].last_used)).toBeInTheDocument(); + } else { + expect(mock_connected_apps[0].last_used).not.toBeNull(); + } + expect(screen.getByText('Permission')).toBeInTheDocument(); + if (mock_permissions) { + expect(screen.getByText(mock_permissions)).toBeInTheDocument(); + } else { + expect(mock_permissions).not.toBeNull(); + } + expect(screen.getByRole('button', { name: 'Revoke access' })).toBeInTheDocument(); + }); + }); + + it('should render the app list in Mobile view', async () => { + renderComponent(mockStore({ ui: { is_mobile: true } })); + const mock_permissions = mock_connected_apps[0]?.scopes + ?.map(scope => scope.charAt(0).toUpperCase().concat(scope.substring(1))) + .join(', '); + + await waitFor(() => { + expect(screen.getByText('Name')).toBeInTheDocument(); + expect(screen.getByText(mock_connected_apps[0].name)).toBeInTheDocument(); + expect(screen.getByText('Last login')).toBeInTheDocument(); + if (mock_connected_apps[0]?.last_used) { + expect(screen.getByText(mock_connected_apps[0].last_used)).toBeInTheDocument(); + } else { + expect(mock_connected_apps[0].last_used).not.toBeNull(); + } + expect(screen.getByText('Permission')).toBeInTheDocument(); + if (mock_permissions) { + expect(screen.getByText(mock_permissions)).toBeInTheDocument(); + } else { + expect(mock_permissions).not.toBeNull(); + } + expect(screen.getByRole('button', { name: 'Revoke access' })).toBeInTheDocument(); + }); + }); + + it('should open the modal to revoke access on clicking the button', async () => { + renderComponent(); + + await waitFor(() => { + const revoke_button = screen.getByRole('button', { name: 'Revoke access' }); + expect(revoke_button).toBeInTheDocument(); + userEvent.click(revoke_button); + expect(screen.getByText(/Confirm revoke access\?/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument(); + + const confirm_button = screen.getByRole('button', { name: 'Confirm' }); + expect(confirm_button).toBeInTheDocument(); + userEvent.click(confirm_button); + expect(WS.authorized.send).toBeCalled(); + }); + }); + + it('should render the empty apps informative text component if there are no connected apps', async () => { + (WS.authorized.send as jest.Mock).mockReturnValue({ oauth_apps: [] }); + renderComponent(); + + await waitFor(() => { + expect(screen.getByText(/Mocked Empty Apps/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/data-list-template-entry.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/data-list-template-entry.spec.tsx new file mode 100644 index 000000000000..bf9dba18ff5b --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/data-list-template-entry.spec.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import DataListTemplateEntry from '../data-list-template-entry'; + +describe('DataListTemplateEntry', () => { + it("should render the 'DataListTemplateEntry' component with correct details", () => { + const mock_props: React.ComponentProps = { + title: 'MOCK_TITLE', + content: 'MOCK_CONTENT', + }; + render(); + + expect(screen.getByText(mock_props.title.toString())).toBeInTheDocument(); + expect(screen.getByText(mock_props.content.toString())).toBeInTheDocument(); + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/__tests__/data-list-template.spec.tsx b/packages/account/src/Sections/Security/ConnectedApps/__tests__/data-list-template.spec.tsx new file mode 100644 index 000000000000..22ef1f9dde91 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/__tests__/data-list-template.spec.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import DataListTemplate from '../data-list-template'; + +describe('DataListTemplate', () => { + it("should render the 'DataListTemplate' component with correct details", () => { + const mock_props: React.ComponentProps = { + data_source: { + app_id: 99, + app_markup_percentage: 1, + last_used: '2021-10-31 06:49:52', + name: 'NAME', + official: 0, + scopes: ['read', 'admin'], + appstore: null, + github: null, + googleplay: null, + homepage: null, + redirect_uri: '', + verification_uri: null, + }, + handleToggleModal: () => undefined, + }; + const mock_permissions = mock_props.data_source?.scopes + ?.map(scope => scope.charAt(0).toUpperCase().concat(scope.substring(1))) + .join(', '); + render(); + + expect(screen.getByText(mock_props.data_source.name)).toBeInTheDocument(); + if (mock_props.data_source?.last_used) { + expect(screen.getByText(mock_props.data_source?.last_used)).toBeInTheDocument(); + } else { + expect(mock_props.data_source?.last_used).not.toBeNull(); + } + if (mock_permissions) { + expect(screen.getByText(mock_permissions)).toBeInTheDocument(); + } else { + expect(mock_permissions).not.toBeNull(); + } + }); +}); diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-earn-more.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-earn-more.tsx new file mode 100644 index 000000000000..c2639e67a43e --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-earn-more.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Localize } from '@deriv/translations'; +import Article from 'Components/article'; + +const openDerivAPIWebsite = () => { + window.open('https://api.deriv.com/', '_blank', 'noopener'); +}; + +const ConnectedAppsEarnMore = () => ( +
} + descriptions={[ + , + ]} + onClickLearnMore={openDerivAPIWebsite} + /> +); + +export default ConnectedAppsEarnMore; diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-empty.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-empty.tsx new file mode 100644 index 000000000000..3b2c98efc12e --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-empty.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Text } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; +import { Localize } from '@deriv/translations'; +import ConnectedAppsInfoBullets from './connected-apps-info-bullets'; + +const ConnectedAppsEmpty = observer(() => { + const { ui } = useStore(); + const { is_mobile } = ui; + + const text_size = is_mobile ? 'xxs' : 'xs'; + + return ( +
+ + + + +
+ ); +}); + +export default ConnectedAppsEmpty; diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-info-bullets.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-info-bullets.tsx new file mode 100644 index 000000000000..9236d9d63d9b --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-info-bullets.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Text } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; +import { CONNECTED_APPS_INFO_BULLETS } from 'Constants/connected-apps-config'; + +type TConnectedAppsInfoBulletsProps = { + class_name: string; + text_color?: string; +}; + +const ConnectedAppsInfoBullets = observer(({ class_name, text_color }: TConnectedAppsInfoBulletsProps) => { + const { ui } = useStore(); + const { is_mobile } = ui; + + const text_size = is_mobile ? 'xxxs' : 'xxs'; + + return ( + + {CONNECTED_APPS_INFO_BULLETS.map(bullet => ( +
  • {bullet.text}
  • + ))} +
    + ); +}); + +export default ConnectedAppsInfoBullets; diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-info.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-info.tsx new file mode 100644 index 000000000000..498bcc9efccf --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-info.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { InlineMessage, Text } from '@deriv/components'; +import { Localize } from '@deriv/translations'; +import { observer, useStore } from '@deriv/stores'; +import ConnectedAppsInfoBullets from './connected-apps-info-bullets'; + +const ConnectedAppsInfo = observer(() => { + const { ui } = useStore(); + const { is_mobile } = ui; + + const text_size = is_mobile ? 'xxxs' : 'xxs'; + + return ( + + + + + + + } + /> + ); +}); + +export default ConnectedAppsInfo; diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-article.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-know-more.tsx similarity index 63% rename from packages/account/src/Sections/Security/ConnectedApps/connected-apps-article.tsx rename to packages/account/src/Sections/Security/ConnectedApps/connected-apps-know-more.tsx index 708976db834d..32910ec875b3 100644 --- a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-article.tsx +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-know-more.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { localize, Localize } from '@deriv/translations'; -import AccountArticle from 'Components/article'; +import { Localize } from '@deriv/translations'; +import Article from 'Components/article'; const openAPIManagingWebsite = () => { window.open( @@ -10,10 +10,9 @@ const openAPIManagingWebsite = () => { ); }; -const ConnectedAppsArticle = () => ( - ( +
    } descriptions={[ ( /> ); -export default ConnectedAppsArticle; +export default ConnectedAppsKnowMore; diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps-revoke-modal.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-revoke-modal.tsx new file mode 100644 index 000000000000..081fcacd1795 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps-revoke-modal.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Button, Icon, Modal, Text } from '@deriv/components'; +import { Localize } from '@deriv/translations'; + +type TConnectedAppsRevokeModalProps = { + handleRevokeAccess: () => void; + handleToggleModal: (app_id?: number | null) => void; + is_modal_open: boolean; +}; + +const ConnectedAppsRevokeModal = ({ + handleRevokeAccess, + handleToggleModal, + is_modal_open, +}: TConnectedAppsRevokeModalProps) => ( + + +
    +
    + + + + +
    +
    + + +
    +
    +
    +
    +); + +export default ConnectedAppsRevokeModal; diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps.scss b/packages/account/src/Sections/Security/ConnectedApps/connected-apps.scss new file mode 100644 index 000000000000..ecb30795bbf4 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps.scss @@ -0,0 +1,188 @@ +.connected-apps { + &__wrapper { + display: grid; + gap: 1.6rem; + grid-template-rows: min-content; + grid-template-columns: 1fr min-content; + @include mobile { + grid-template-columns: 1fr; + padding: 1.6rem; + } + } + + &__content { + &--wrapper { + display: flex; + flex-direction: column; + gap: 2.4rem; + @include mobile { + gap: 1.6rem; + } + } + } + + &__list { + &--wrapper { + display: flex; + flex-direction: column; + gap: 0.8rem; + } + + &--row { + display: flex; + justify-content: space-between; + gap: 1.6rem; + } + + &--template { + background: var(--general-section-1); + border-radius: $BORDER_RADIUS * 2; + padding: 1.6rem; + } + + &--name, + &--last-login, + &--permission, + &--revoke { + display: flex; + flex-direction: column; + } + + &--last-login { + width: 8rem; + } + + &--revoke { + justify-content: flex-end; + } + } + + &__tabular { + &--wrapper { + height: 36rem; + } + } + + &__articles { + &--wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.6rem; + margin-left: 0.8rem; + @include mobile { + margin-left: 0; + } + + .da-article { + margin: 0; + @include mobile { + width: 100%; + } + } + } + } + + &__empty { + &--wrapper { + display: flex; + flex-direction: column; + gap: 0.8rem; + margin-top: 9.6rem; + @include mobile { + gap: 1.6rem; + margin-top: 0; + } + } + } + + &__bullets { + &--list { + list-style: auto; + display: flex; + flex-direction: column; + } + + &--with-apps { + gap: 1.4rem; + padding: 1.4rem 0 0 1.4rem; + @include mobile { + padding: 1rem 0 0 1rem; + } + } + + &--without-apps { + padding: 0 1.6rem; + gap: 0.8rem; + @include mobile { + gap: 0.4rem; + } + } + } + + // Styling for DataTable displayed in Desktop view + .table__body .table__row { + border-bottom: 1px solid var(--general-section-1); + } + + .table__head .table__row { + font-weight: bold; + } + + &__table { + height: 100%; + flex: 1; + } + + &__row { + grid-template-columns: 16rem 20rem 8rem 10rem; + padding: 0; + column-gap: 4.2rem; + text-align: left; + + .table__cell { + min-height: 5.6rem; + white-space: pre-wrap; + padding: 0; + + .name { + &__content { + text-overflow: ellipsis; + width: 100%; + overflow: hidden; + } + } + } + } +} + +.dc-modal { + &__container { + &_connected-apps { + .dc-modal { + &-body { + padding: 2.4rem; + .connected-apps-modal { + &--wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 2.4rem; + } + + &--icon { + display: flex; + flex-direction: column; + align-items: center; + } + + &--buttons { + display: flex; + gap: 0.8rem; + } + } + } + } + } + } +} diff --git a/packages/account/src/Sections/Security/ConnectedApps/connected-apps.tsx b/packages/account/src/Sections/Security/ConnectedApps/connected-apps.tsx index 0da56348e2e5..8c9018358b22 100644 --- a/packages/account/src/Sections/Security/ConnectedApps/connected-apps.tsx +++ b/packages/account/src/Sections/Security/ConnectedApps/connected-apps.tsx @@ -1,29 +1,29 @@ import React from 'react'; -import classNames from 'classnames'; -import { - DesktopWrapper, - MobileWrapper, - Button, - Modal, - Icon, - DataTable, - DataList, - Loading, - Text, -} from '@deriv/components'; -import ConnectedAppsArticle from './connected-apps-article'; -import { PlatformContext, WS } from '@deriv/shared'; -import { localize } from '@deriv/translations'; +import { OauthApps } from '@deriv/api-types'; +import { DataTable, Loading } from '@deriv/components'; +import { observer, useStore } from '@deriv/stores'; +import { WS } from '@deriv/shared'; import ErrorComponent from 'Components/error-component'; -import GetConnectedAppsColumnsTemplate from './data-table-template'; +import ConnectedAppsKnowMore from './connected-apps-know-more'; +import ConnectedAppsInfo from './connected-apps-info'; +import ConnectedAppsEarnMore from './connected-apps-earn-more'; +import ConnectedAppsEmpty from './connected-apps-empty'; +import DataListTemplate from './data-list-template'; +import DataTableTemplate from './data-table-template'; +import ConnectedAppsRevokeModal from './connected-apps-revoke-modal'; +import './connected-apps.scss'; + +type TSource = React.ComponentProps['columns']; + +const ConnectedApps = observer(() => { + const { ui } = useStore(); + const { is_mobile } = ui; -const ConnectedApps = () => { - const { is_appstore } = React.useContext(PlatformContext); const [is_loading, setLoading] = React.useState(true); - const [is_modal_open, setModalVisibility] = React.useState(false); - const [selected_app_id, setAppId] = React.useState(null); + const [is_modal_open, setIsModalOpen] = React.useState(false); + const [selected_app_id, setSelectedAppId] = React.useState(null); const [is_error, setError] = React.useState(false); - const [connected_apps, setConnectedApps] = React.useState([]); + const [connected_apps, setConnectedApps] = React.useState([]); React.useEffect(() => { /* eslint-disable no-console */ @@ -39,40 +39,10 @@ const ConnectedApps = () => { } }; - const handleToggleModal = React.useCallback( - (app_id: number | null = null) => { - setModalVisibility(!is_modal_open); - setAppId(app_id); - }, - [is_modal_open] - ); - - type TColumn = ReturnType[number]; - - const columns_map = React.useMemo( - () => - GetConnectedAppsColumnsTemplate(app_id => handleToggleModal(app_id)).reduce((map, item) => { - map[item.col_index] = item; - return map; - }, {} as { [k in TColumn['col_index']]: TColumn }), - [handleToggleModal] - ); - - const mobileRowRenderer = React.useCallback( - ({ row }: { row: TColumn['renderCellContent'] }) => ( -
    -
    - - -
    -
    - - -
    -
    - ), - [columns_map, is_appstore] - ); + const handleToggleModal = React.useCallback((app_id: number | null = null) => { + setIsModalOpen(is_modal_open => !is_modal_open); + setSelectedAppId(app_id); + }, []); const revokeConnectedApp = React.useCallback(async (app_id: number | null) => { setLoading(true); @@ -81,83 +51,61 @@ const ConnectedApps = () => { /* eslint-disable no-console */ fetchConnectedApps().catch(error => console.error('error: ', error)); } else { + setLoading(false); setError(true); } }, []); const handleRevokeAccess = React.useCallback(() => { - setModalVisibility(false); + setIsModalOpen(false); revokeConnectedApp(selected_app_id); }, [revokeConnectedApp, selected_app_id]); - return ( -
    - - {localize('Authorised applications')} - - {is_error && } -
    - {is_loading ? ( - + return is_loading ? ( + + ) : ( +
    +
    + {is_error && } + {connected_apps.length ? ( +
    + + {is_mobile ? ( +
    + {connected_apps.map(connected_app => ( + + ))} +
    + ) : ( +
    + +
    + )} +
    ) : ( - - - - - - - - + )} - - {!is_loading && !!connected_apps.length && } -
    - - - -
    - - - {localize('Confirm revoke access?')} - -
    - - -
    -
    -
    -
    -
    + +
    + + +
    + + ); -}; +}); export default ConnectedApps; diff --git a/packages/account/src/Sections/Security/ConnectedApps/data-list-template-entry.tsx b/packages/account/src/Sections/Security/ConnectedApps/data-list-template-entry.tsx new file mode 100644 index 000000000000..961acb1673c6 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/data-list-template-entry.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Text } from '@deriv/components'; + +type TDataListTemplateEntry = { + title: JSX.Element | string; + content: JSX.Element | string; +}; + +const DataListTemplateEntry = ({ title, content }: TDataListTemplateEntry) => ( + + + {title} + + {content} + +); + +export default DataListTemplateEntry; diff --git a/packages/account/src/Sections/Security/ConnectedApps/data-list-template.tsx b/packages/account/src/Sections/Security/ConnectedApps/data-list-template.tsx new file mode 100644 index 000000000000..f4a6ec5fad50 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/data-list-template.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { ApplicationObject } from '@deriv/api-types'; +import { Button } from '@deriv/components'; +import { toMoment } from '@deriv/shared'; +import { Localize } from '@deriv/translations'; +import DataListTemplateEntry from './data-list-template-entry'; +import { getConnectedAppsScopes } from './template-helper'; + +type TDataListTemplate = { data_source: ApplicationObject; handleToggleModal: (app_id: number) => void }; + +const DataListTemplate = ({ data_source, handleToggleModal }: TDataListTemplate) => ( +
    +
    +
    + } content={data_source.name} /> +
    +
    + } + content={toMoment(data_source.last_used).format('YYYY-MM-DD HH:mm:ss')} + /> +
    +
    +
    +
    + } + content={getConnectedAppsScopes(data_source.scopes ?? [])} + /> +
    +
    + +
    +
    +
    +); + +export default DataListTemplate; diff --git a/packages/account/src/Sections/Security/ConnectedApps/data-table-template.tsx b/packages/account/src/Sections/Security/ConnectedApps/data-table-template.tsx index b38f10eceece..38c8ae9b86c7 100644 --- a/packages/account/src/Sections/Security/ConnectedApps/data-table-template.tsx +++ b/packages/account/src/Sections/Security/ConnectedApps/data-table-template.tsx @@ -2,7 +2,8 @@ import React from 'react'; import { toMoment } from '@deriv/shared'; import { Button, Text } from '@deriv/components'; -import { localize } from '@deriv/translations'; +import { Localize, localize } from '@deriv/translations'; +import { getConnectedAppsScopes } from './template-helper'; type Column = { title: string; @@ -10,17 +11,11 @@ type Column = { renderCellContent: React.FC<{ cell_value: string & number & string[] }>; }; -type TGetConnectedAppsColumnsTemplate = { +type TDataTableTemplateProps = { handleToggleModal: (app_id: number | null) => void; }; -type Permissions = { - [key: string]: string; -}; - -const GetConnectedAppsColumnsTemplate = ( - handleToggleModal: TGetConnectedAppsColumnsTemplate['handleToggleModal'] -): Column[] => [ +const DataTableTemplate = ({ handleToggleModal }: TDataTableTemplateProps): Column[] => [ { title: localize('Name'), col_index: 'name', @@ -36,64 +31,31 @@ const GetConnectedAppsColumnsTemplate = ( title: localize('Permission'), col_index: 'scopes', renderCellContent: ({ cell_value }) => { - return PrepareConnectedAppsScopes(cell_value); + return getConnectedAppsScopes(cell_value); }, }, { title: localize('Last login'), col_index: 'last_used', - renderCellContent: ({ cell_value }) => PrepareConnectedAppsLastLogin(cell_value), + renderCellContent: ({ cell_value }) => getConnectedAppsLastLogin(cell_value), }, { title: localize('Action'), col_index: 'app_id', - renderCellContent: ({ cell_value }) => PrepareConnectedAppsAction(cell_value, handleToggleModal), + renderCellContent: ({ cell_value }) => getConnectedAppsAction(cell_value, handleToggleModal), }, ]; -const PrepareConnectedAppsAction = ( - app_id: number, - handleToggleModal: TGetConnectedAppsColumnsTemplate['handleToggleModal'] -) => { - return ( - - ); -}; +const getConnectedAppsAction = (app_id: number, handleToggleModal: TDataTableTemplateProps['handleToggleModal']) => ( + +); -const PrepareConnectedAppsLastLogin = (last_used: number) => ( +const getConnectedAppsLastLogin = (last_used: number) => ( {toMoment(last_used).format('YYYY-MM-DD HH:mm:ss')} ); -const generatePermissions = (): Permissions => ({ - read: localize('Read'), - trade: localize('Trade'), - trading_information: localize('Trading information'), - payments: localize('Payments'), - admin: localize('Admin'), -}); - -const PrepareConnectedAppsScopes = (permissions_list: string[]) => { - const is_trading_information = permissions_list.includes('trading_information'); - let oauth_apps_list = []; - if (is_trading_information) { - oauth_apps_list = permissions_list.filter(permission => permission !== 'trading_information'); - oauth_apps_list.push('trading_information'); - } else { - oauth_apps_list = permissions_list; - } - const sorted_app_list: string[] = []; - oauth_apps_list.forEach((permission, index) => { - if (permissions_list.length - 1 !== index) { - sorted_app_list.push(`${generatePermissions()[permission]}, `); - } else { - sorted_app_list.push(generatePermissions()[permission]); - } - }); - return
    {sorted_app_list}
    ; -}; - -export default GetConnectedAppsColumnsTemplate; +export default DataTableTemplate; diff --git a/packages/account/src/Sections/Security/ConnectedApps/index.js b/packages/account/src/Sections/Security/ConnectedApps/index.ts similarity index 100% rename from packages/account/src/Sections/Security/ConnectedApps/index.js rename to packages/account/src/Sections/Security/ConnectedApps/index.ts diff --git a/packages/account/src/Sections/Security/ConnectedApps/template-helper.tsx b/packages/account/src/Sections/Security/ConnectedApps/template-helper.tsx new file mode 100644 index 000000000000..1c0f80e92e46 --- /dev/null +++ b/packages/account/src/Sections/Security/ConnectedApps/template-helper.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { localize } from '@deriv/translations'; + +type Permissions = { + [key: string]: string; +}; + +export const generatePermissions = (): Permissions => ({ + read: localize('Read'), + trade: localize('Trade'), + trading_information: localize('Trading information'), + payments: localize('Payments'), + admin: localize('Admin'), +}); + +export const getConnectedAppsScopes = (permissions_list: string[]) => { + const is_trading_information = permissions_list.includes('trading_information'); + let oauth_apps_list = []; + if (is_trading_information) { + oauth_apps_list = permissions_list.filter(permission => permission !== 'trading_information'); + oauth_apps_list.push('trading_information'); + } else { + oauth_apps_list = permissions_list; + } + const sorted_app_list: string[] = []; + oauth_apps_list.forEach((permission, index) => { + if (permissions_list.length - 1 !== index) { + sorted_app_list.push(`${generatePermissions()[permission]}, `); + } else { + sorted_app_list.push(generatePermissions()[permission]); + } + }); + return
    {sorted_app_list}
    ; +}; diff --git a/packages/account/src/Styles/account.scss b/packages/account/src/Styles/account.scss index 56c50c6ccaef..9463c407e159 100644 --- a/packages/account/src/Styles/account.scss +++ b/packages/account/src/Styles/account.scss @@ -1579,230 +1579,6 @@ $MIN_HEIGHT_FLOATING: calc( } } -/** @define connected-apps; weak */ -.connected-apps { - width: 100%; - height: 100%; - - .revoke_access { - padding-left: 8px; - padding-right: 8px; - left: -5%; - } - - &__loading { - height: calc(100vh - 240px); - } - - &__title { - margin-bottom: 1.6rem; - } - - &__table { - height: calc(100% - 42px); - flex: 1; - max-width: 100%; - max-height: 502px; - } - - &__row { - grid-template-columns: 169px 198px 80px 104px; - padding: 0; - column-gap: 42px; - text-align: left; - - .table__cell { - min-height: 5.6rem; - white-space: pre-wrap; - padding: 0; - - .name { - &__content { - text-overflow: ellipsis; - width: 100%; - overflow: hidden; - } - } - } - } - - &__wrapper { - max-height: 560px; - height: 100%; - width: 100%; - - &--dashboard { - max-width: 80rem; - - & .table__cell { - height: 3.8rem; - } - - & .table__row { - grid-template-columns: 17rem 20rem 15.5rem 11rem; - } - - & .data-list__item { - box-shadow: 0 1.6rem #00000005, 0 1.6rem #0000000d; - } - } - } - - &__container { - display: flex; - - @include mobile { - flex-direction: column-reverse; - } - } - - &__article { - margin-top: -4rem; - - @include mobile { - margin-top: -0.8rem; - } - } - - .table__body .table__row { - border-bottom: 1px solid var(--general-section-1); - } - - @include desktop { - &__row { - padding: 0; - } - - .table__cell { - padding: 0; - } - - .table__row { - padding: 0; - } - - .table__head { - font-weight: bold; - } - } - - @include mobile { - overflow-x: hidden; - padding: 0 1.6rem; - - /* iPhone SE screen height fixes due to UI space restrictions */ - @media only screen and (max-height: 480px) { - padding-bottom: 8.5rem; - } - - &__title { - margin: 2.4rem 0; - padding: 0 1.6rem; - } - - &__data-list { - padding: 0; - } - - &__data-list-body { - min-height: calc(100vh - 50px); - - .connected-apps { - max-height: calc(100vh - 130px); - } - - .data-list__item { - padding: 8px; - background: var(--general-section-1); - } - - .data-list__row { - display: flex; - justify-content: space-between; - padding: 0; - - &-content { - word-break: break-word; - } - - .data-list__col { - width: 50%; - margin-right: 1.8rem; - - > :first-child { - margin-bottom: 8px; - } - - &--small { - max-width: 99px; - - .data-list__row-content .dc-btn { - left: 50%; - transform: translateX(-53%); - } - - .last_used__row-title { - line-height: 1.5; - } - } - - &--dashboard { - max-width: 14.2rem; - - & .data-list__row-title { - text-align: right; - line-height: 1.5; - } - - & .data-list__row-content { - float: right; - } - - @include mobile { - display: grid; - } - } - - .data-list__row-title { - line-height: 1.5; - } - } - } - } - - &__wrapper--dashboard { - & .ReactVirtualized__Grid__innerScrollContainer { - box-shadow: 0 0 20px rgba(0, 0, 0, 0.05), 0 16px 20px rgba(0, 0, 0, 0.05); - border-radius: 0.4rem; - } - - & .data-list__col { - display: grid; - } - - & .data-list__item { - background: unset; - } - } - - .name { - &__content { - text-overflow: ellipsis; - width: 100%; - display: inline-block; - overflow: hidden; - } - } - - .last_used { - .last_used_content { - margin-bottom: 4px; - margin-top: 2px; - } - } - } -} - /** @define initial-loader; weak */ .initial-loader--btn { .initial-loader__barspinner--rect { @@ -1895,47 +1671,6 @@ $MIN_HEIGHT_FLOATING: calc( padding-top: 1.2rem; } } - - &_connected-apps { - .dc-modal-body { - padding: 0; - width: 440px; - display: flex; - justify-content: center; - - .connected-app-modal { - text-align: center; - - &__icon { - margin: 24px 0; - } - - &__confirmation { - margin-top: 24px; - margin-bottom: 32px; - - button { - min-width: 85px; - height: 40px; - } - - > :first-child { - margin-right: 16px; - } - - &-dashboard { - button { - width: unset; - } - } - } - } - - @include mobile { - width: 300px; - } - } - } } .leave-confirm { diff --git a/packages/components/src/components/icon/common/ic-account-trash-can-dashboard.svg b/packages/components/src/components/icon/common/ic-account-trash-can-dashboard.svg deleted file mode 100644 index f2953e9e3a03..000000000000 --- a/packages/components/src/components/icon/common/ic-account-trash-can-dashboard.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/components/src/components/icon/icons.js b/packages/components/src/components/icon/icons.js index 3ba72c9af31a..a2955cc71a72 100644 --- a/packages/components/src/components/icon/icons.js +++ b/packages/components/src/components/icon/icons.js @@ -249,7 +249,6 @@ import './common/ic-account-missing-details.svg'; import './common/ic-account-tick.svg'; import './common/ic-account-transfer-colored.svg'; import './common/ic-account-transfer.svg'; -import './common/ic-account-trash-can-dashboard.svg'; import './common/ic-account-trash-can.svg'; import './common/ic-account-website.svg'; import './common/ic-add-account.svg'; diff --git a/packages/components/stories/icon/icons.js b/packages/components/stories/icon/icons.js index 7bd59c50adb7..40eaf5433560 100644 --- a/packages/components/stories/icon/icons.js +++ b/packages/components/stories/icon/icons.js @@ -258,7 +258,6 @@ export const icons = 'IcAccountTick', 'IcAccountTransferColored', 'IcAccountTransfer', - 'IcAccountTrashCanDashboard', 'IcAccountTrashCan', 'IcAccountWebsite', 'IcAddAccount',