-
- Edit application details
-
-
- Delete application
-
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/features/dashboard/components/AppsTable/apps-table.scss b/src/features/dashboard/components/AppsTable/apps-table.scss
new file mode 100644
index 000000000..ff1e5a8ff
--- /dev/null
+++ b/src/features/dashboard/components/AppsTable/apps-table.scss
@@ -0,0 +1,82 @@
+.apps_table {
+ border: 1px solid var(--opacity-black-100);
+ border-radius: 32px;
+ margin: 48px;
+ margin-top: 0;
+
+ &.mobile {
+ border: none;
+ margin: 0px;
+ }
+
+ table {
+ table-layout: fixed;
+ border-collapse: collapse;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-inline: 48px;
+
+ th,
+ td,
+ tr {
+ border: 0px;
+ border-bottom: 1px solid var(--solid-slate-75);
+ text-align: left;
+ height: 72px;
+ padding: 8px 16px;
+ }
+ tr {
+ background-color: transparent;
+ font-weight: 400;
+ }
+ }
+
+ &__table_container {
+ position: relative;
+ max-height: 560px;
+ overflow-y: auto;
+ }
+
+ &__table_header {
+ table-layout: fixed;
+ border-collapse: collapse;
+ th {
+ background-color: var(--solid-slate-75);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+ font-weight: bold;
+ }
+ }
+
+ &__table_body {
+ width: 100%;
+ overflow-y: auto;
+ }
+
+ &__header {
+ display: flex;
+ justify-content: space-between;
+ padding: 48px;
+
+ &.mobile {
+ flex-direction: column;
+ align-items: center;
+ text-align: center;
+ padding: unset;
+ }
+ &__button {
+ margin-top: 16px;
+ }
+
+ &__texts {
+ display: block;
+ max-width: 72%;
+
+ h3 {
+ margin-bottom: 16px;
+ }
+ }
+ }
+}
diff --git a/src/features/dashboard/components/AppsTable/cells.module.scss b/src/features/dashboard/components/AppsTable/cells.module.scss
index ff96715b7..4e068e391 100644
--- a/src/features/dashboard/components/AppsTable/cells.module.scss
+++ b/src/features/dashboard/components/AppsTable/cells.module.scss
@@ -1,68 +1,15 @@
@use 'src/styles/utility' as *;
-@mixin actionIcon {
- background-repeat: no-repeat;
- background-position: center;
- background-size: rem(1.8);
- cursor: pointer;
- padding: rem(1.8) rem(1.8);
- border-radius: 100%;
-}
-
-.deleteApp {
- background-image: url(/img/delete.svg);
- @include actionIcon;
-}
-
-.updateApp {
- background-image: url(/img/edit.svg);
- @include actionIcon;
-}
-
.appActions {
+ width: 168px;
display: flex;
- margin: rem(3);
- justify-content: center;
-}
-
-.tooltip {
- position: relative;
-
- .tooltipText {
- visibility: hidden;
- color: var(--ifm-color-emphasis-100);
- background-color: var(--ifm-color-emphasis-700);
- text-align: center;
- border-radius: 4px;
- position: absolute;
- z-index: 1;
- opacity: 0;
- transition: opacity 0.3s;
- font-size: rem(1);
- transform: translateX(50%);
- right: 50%;
- bottom: rem(4);
- padding: rem(0.5);
- &::after {
- content: '';
- position: absolute;
- bottom: rem(-0.9);
- right: 50%;
- transform: translateX(50%);
- margin-left: rem(-0.5);
- border-width: 5px;
- border-style: solid;
- border-color: var(--ifm-color-emphasis-700) transparent transparent transparent;
- }
+ svg {
+ margin-inline: 8px;
+ cursor: pointer;
}
+}
- &:hover {
- transform: translateY(-0.2rem);
-
- .tooltipText {
- visibility: visible;
- opacity: 1;
- }
- }
+.flex_end {
+ justify-content: flex-end;
}
diff --git a/src/features/dashboard/components/AppsTable/index.tsx b/src/features/dashboard/components/AppsTable/index.tsx
index 125524c72..8ba091907 100644
--- a/src/features/dashboard/components/AppsTable/index.tsx
+++ b/src/features/dashboard/components/AppsTable/index.tsx
@@ -1,32 +1,48 @@
import { ApplicationObject } from '@deriv/api-types';
import React, { HTMLAttributes, useCallback, useState } from 'react';
import { Cell, Column } from 'react-table';
-import NoApps from '../NoApps';
+import { Button, Heading, Text } from '@deriv/quill-design';
+import { LabelPairedCirclePlusMdRegularIcon } from '@deriv/quill-icons';
+
+import useAppManager from '@site/src/hooks/useAppManager';
+import useDeviceType from '@site/src/hooks/useDeviceType';
+import ResponsiveTable from './responsive-table';
+import AppActionsCell from './app-actions.cell';
+import CopyTextCell from '../Table/copy-text.cell';
import DeleteAppDialog from '../Dialogs/DeleteAppDialog';
-import UpdateAppDialog from '../Dialogs/UpdateAppDialog';
-import Table from '../Table';
import ScopesCell from '../Table/scopes.cell';
-import AppActionsCell from './app-actions.cell';
+import Table from '../Table';
+import UpdateAppDialog from '../Dialogs/UpdateAppDialog';
+import clsx from 'clsx';
+import './apps-table.scss';
export type TAppColumn = Column
;
const appTableColumns: TAppColumn[] = [
{
- Header: 'Application Name',
+ Header: 'App’s name',
accessor: 'name',
+ minWidth: 150,
+ maxWidth: 200,
},
{
- Header: 'Application ID',
+ Header: 'App ID',
accessor: 'app_id',
+ minWidth: 120,
+ maxWidth: 150,
+ Cell: CopyTextCell,
},
{
- Header: 'Scopes',
+ Header: 'OAuth scopes',
accessor: 'scopes',
+ minWidth: 200,
Cell: ScopesCell,
},
{
- Header: 'Redirect URL',
+ Header: 'OAuth redirect URL',
accessor: 'redirect_uri',
+ minWidth: 350,
+ Cell: CopyTextCell,
},
{
Header: 'Actions',
@@ -40,25 +56,75 @@ interface AppsTableProps extends HTMLAttributes {
apps: ApplicationObject[];
}
+const AppsTableHeader: React.FC<{ is_desktop: boolean }> = ({ is_desktop }) => {
+ const { updateCurrentTab } = useAppManager();
+
+ return (
+
+
+ App manager
+
+ Here's where you can see your app's details. Edit your app settings to suit your
+ needs or delete them permanently.
+
+
+
+
+ );
+};
+
const AppsTable = ({ apps }: AppsTableProps) => {
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [actionRow, setActionRow] = useState();
+ const { deviceType } = useDeviceType();
+ const is_desktop = deviceType === 'desktop';
- const getCustomCellProps = useCallback((cell: Cell) => {
+ const getActionObject = useCallback((item: ApplicationObject) => {
return {
openDeleteDialog: () => {
- setActionRow(cell.row.original);
- setIsDeleteOpen(true);
+ setActionRow(item);
+ // setIsDeleteOpen(true);
},
openEditDialog: () => {
- setActionRow(cell.row.original);
- setIsEditOpen(true);
+ setActionRow(item);
+ // setIsEditOpen(true);
},
};
}, []);
+ const getCustomCellProps = useCallback(
+ (cell: Cell) => {
+ return getActionObject(cell.row.original);
+ },
+ [getActionObject],
+ );
+
+ const accordionActions = useCallback(
+ (item: ApplicationObject) => {
+ return getActionObject(item);
+ },
+ [getActionObject],
+ );
+
const onCloseEdit = () => {
setActionRow(null);
setIsEditOpen(false);
@@ -69,16 +135,33 @@ const AppsTable = ({ apps }: AppsTableProps) => {
setIsDeleteOpen(false);
};
- if (apps.length) {
- return (
- <>
- {isDeleteOpen && }
- {isEditOpen && }
-
- >
+ const renderTable = () => {
+ return is_desktop ? (
+
+ ) : (
+
);
- }
- return ;
+ };
+
+ return (
+
+ {isDeleteOpen &&
}
+ {isEditOpen &&
}
+
+
+ {apps?.length ? renderTable() : null}
+
+
+ );
};
export default AppsTable;
diff --git a/src/features/dashboard/components/AppsTable/responsive-table.scss b/src/features/dashboard/components/AppsTable/responsive-table.scss
new file mode 100644
index 000000000..c8996d7ce
--- /dev/null
+++ b/src/features/dashboard/components/AppsTable/responsive-table.scss
@@ -0,0 +1,30 @@
+.accordion_item {
+ width: 100%;
+ padding-block: 18px;
+ border-bottom: 1px solid var(--opacity-black-75);
+ font-size: 14px;
+
+ &_column {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ &__label {
+ line-height: 2;
+ min-width: fit-content;
+ font-weight: 500;
+ }
+ &__value {
+ text-align: end;
+ justify-content: end;
+ &_row {
+ text-align: start;
+ justify-content: start;
+ overflow-wrap: anywhere;
+ }
+ }
+ .redirect_url {
+ text-align: start;
+ }
+}
diff --git a/src/features/dashboard/components/AppsTable/responsive-table.tsx b/src/features/dashboard/components/AppsTable/responsive-table.tsx
new file mode 100644
index 000000000..cd0ab30c0
--- /dev/null
+++ b/src/features/dashboard/components/AppsTable/responsive-table.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import CustomAccordion from '@site/src/components/CustomAccordion';
+import { ApplicationObject } from '@deriv/api-types';
+import CopyTextCell from '../Table/copy-text.cell';
+import ScopesCell from '../Table/scopes.cell';
+import AppActionsCell from './app-actions.cell';
+import clsx from 'clsx';
+import './responsive-table.scss';
+
+type TResponsiveTableProps = {
+ apps: ApplicationObject[];
+ accordionActions: TAccordionActions;
+};
+
+type TAccordionActions = (item: ApplicationObject) => {
+ openDeleteDialog: () => void;
+ openEditDialog: () => void;
+};
+
+type TAccordionItemProps = {
+ label: string;
+ value: React.ReactNode;
+ row_wise?: boolean;
+};
+
+const AccordionItem: React.FC = ({ label, value, row_wise = false }) => (
+
+
{label}
+
+ {value}
+
+
+);
+
+const generateContent = (item: ApplicationObject, accordionActions: TAccordionActions) => {
+ return (
+
+
} />
+
+ }
+ />
+
}
+ row_wise
+ />
+
+ }
+ />
+
+ );
+};
+
+const ResponsiveTable = ({ apps, accordionActions }: TResponsiveTableProps) => {
+ const items = apps.map((app) => ({
+ header: app.name,
+ content: generateContent(app, accordionActions),
+ }));
+ return ;
+};
+
+export default ResponsiveTable;
diff --git a/src/features/dashboard/components/Modals/AppRegisterSuccessModal/__tests__/app-register-success-modal.test.tsx b/src/features/dashboard/components/Modals/AppRegisterSuccessModal/__tests__/app-register-success-modal.test.tsx
new file mode 100644
index 000000000..94e101cac
--- /dev/null
+++ b/src/features/dashboard/components/Modals/AppRegisterSuccessModal/__tests__/app-register-success-modal.test.tsx
@@ -0,0 +1,68 @@
+import React from 'react';
+import { cleanup, render, screen } from '@site/src/test-utils';
+import { AppRegisterSuccessModal } from '..';
+import useAppManager from '@site/src/hooks/useAppManager';
+
+const mock_cancel = jest.fn();
+const mock_configure = jest.fn();
+
+jest.mock('@site/src/hooks/useAppManager');
+const mockUseAppManager = useAppManager as jest.MockedFunction<
+ () => Partial>
+>;
+mockUseAppManager.mockImplementation(() => ({
+ app_register_modal_open: true,
+}));
+
+describe('AppRegisterSuccessModal', () => {
+ afterEach(() => {
+ cleanup();
+ jest.clearAllMocks();
+ });
+
+ it('Should render the success modal in desktop', () => {
+ render(
+ ,
+ );
+
+ const label = screen.getByText(/Application registered successfully!/i);
+ expect(label).toBeInTheDocument();
+ const imgElement = screen.getByAltText('check icon');
+ expect(imgElement).toBeInTheDocument();
+ });
+
+ it('Should render the success modal in mobile', () => {
+ render(
+ ,
+ );
+
+ const label = screen.getByText(/Application registered successfully!/i);
+ expect(label).toBeInTheDocument();
+ const imgElement = screen.queryByAltText('check icon');
+ expect(imgElement).not.toBeInTheDocument();
+ });
+
+ it('Should handle click events properly', () => {
+ render(
+ ,
+ );
+ const configure_btn = screen.getByText(/Configure now/i);
+ const maybe_later_btn = screen.getByText(/Maybe later/i);
+ configure_btn.click();
+ expect(mock_configure).toBeCalled();
+ maybe_later_btn.click();
+ expect(mock_cancel).toBeCalled();
+ });
+});
diff --git a/src/features/dashboard/components/Modals/AppRegisterSuccessModal/app-register-success-modal.scss b/src/features/dashboard/components/Modals/AppRegisterSuccessModal/app-register-success-modal.scss
new file mode 100644
index 000000000..036773d3b
--- /dev/null
+++ b/src/features/dashboard/components/Modals/AppRegisterSuccessModal/app-register-success-modal.scss
@@ -0,0 +1,41 @@
+.app_register_success_modal {
+ .action_sheet__main {
+ &.desktop {
+ max-width: 512px;
+ }
+ }
+
+ &__icon {
+ display: flex;
+ justify-content: center;
+ background: var(--solid-slate-75);
+ margin-inline: -16px;
+ margin-top: -16px;
+ margin-bottom: 16px;
+ padding: 24px;
+ border-radius: 12px 12px 0 0;
+ }
+
+ &__header {
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 24px;
+ text-align: center;
+ padding-block: 8px;
+ }
+
+ &__content {
+ margin: 16px;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 24px;
+
+ ul {
+ padding-block: 8px;
+ li {
+ list-style: disc;
+ margin-inline-start: 24px;
+ }
+ }
+ }
+}
diff --git a/src/features/dashboard/components/Modals/AppRegisterSuccessModal/index.tsx b/src/features/dashboard/components/Modals/AppRegisterSuccessModal/index.tsx
new file mode 100644
index 000000000..989fdaed8
--- /dev/null
+++ b/src/features/dashboard/components/Modals/AppRegisterSuccessModal/index.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import useAppManager from '@site/src/hooks/useAppManager';
+import SwipeableBottomSheet from '@site/src/components/SwipeableBottomSheet';
+import { Heading } from '@deriv/quill-design';
+import './app-register-success-modal.scss';
+
+interface IAppRegisterSuccessModalProps {
+ onConfigure: () => void;
+ onCancel: () => void;
+ is_desktop: boolean;
+}
+
+export const AppRegisterSuccessModal = ({
+ onConfigure,
+ onCancel,
+ is_desktop,
+}: IAppRegisterSuccessModalProps) => {
+ const { app_register_modal_open } = useAppManager();
+
+ return (
+
+
+
+ {is_desktop && (
+
+
+
+ )}
+
+ Application registered successfully!
+
+
+
+ Ready to take the next step?
+
Optimise your app's capabilities by:
+
+ - Creating an API token to use with your application.
+ - Adding OAuth authentication in your app.
+ - Selecting the scopes of OAuth authorisation for your app.
+
+ Note: You can make these changes later through the dashboard.
+
+
+
+
+
+ );
+};
diff --git a/src/features/dashboard/components/Table/__tests__/copy-text.cell.test.tsx b/src/features/dashboard/components/Table/__tests__/copy-text.cell.test.tsx
new file mode 100644
index 000000000..3473cdd56
--- /dev/null
+++ b/src/features/dashboard/components/Table/__tests__/copy-text.cell.test.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { render, screen } from '@site/src/test-utils';
+import CopyTextCell from '../copy-text.cell';
+import userEvent from '@testing-library/user-event';
+
+describe('CopyTextCell', () => {
+ beforeAll(() => {
+ Object.assign(navigator, {
+ clipboard: {
+ writeText: jest.fn(),
+ },
+ });
+ });
+
+ it('Should render the copy button', () => {
+ render(
+ ,
+ );
+ const label = screen.getByText(/1234/i);
+ expect(label).toBeInTheDocument();
+ });
+
+ it('Should copy text in the clipboard', async () => {
+ render(
+ ,
+ );
+ const label = screen.getByText(/1234/i);
+ await userEvent.click(label);
+ expect(navigator.clipboard.writeText).toHaveBeenCalledWith('1234');
+ });
+});
diff --git a/src/features/dashboard/components/Table/copy-text.cell.scss b/src/features/dashboard/components/Table/copy-text.cell.scss
new file mode 100644
index 000000000..0e9ac98aa
--- /dev/null
+++ b/src/features/dashboard/components/Table/copy-text.cell.scss
@@ -0,0 +1,9 @@
+.copy_text_cell {
+ display: ruby-text;
+ text-align: left;
+ cursor: pointer;
+
+ &__icon {
+ margin-left: 8px;
+ }
+}
diff --git a/src/features/dashboard/components/Table/copy-text.cell.tsx b/src/features/dashboard/components/Table/copy-text.cell.tsx
new file mode 100644
index 000000000..085a2f96c
--- /dev/null
+++ b/src/features/dashboard/components/Table/copy-text.cell.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { LabelPairedCopyLgRegularIcon } from '@deriv/quill-icons';
+import './copy-text.cell.scss';
+
+const CopyTextCell: React.FC<{
+ cell: {
+ value: React.ReactNode;
+ };
+}> = ({ cell }) => {
+ return (
+
+ {cell.value ? (
+ {
+ navigator.clipboard.writeText(cell.value.toString());
+ }}
+ >
+ {cell.value}
+
+
+
+
+ ) : (
+ ''
+ )}
+
+ );
+};
+
+export default CopyTextCell;
diff --git a/src/features/dashboard/components/Table/index.tsx b/src/features/dashboard/components/Table/index.tsx
index 052fc5a1a..c60e4ec81 100644
--- a/src/features/dashboard/components/Table/index.tsx
+++ b/src/features/dashboard/components/Table/index.tsx
@@ -1,4 +1,4 @@
-import React, { HTMLAttributes, LegacyRef, ReactNode } from 'react';
+import React, { HTMLAttributes } from 'react';
import { Cell, Column, TableState, useTable } from 'react-table';
import './table.scss';
@@ -8,6 +8,7 @@ interface ITableProps extends HTMLAttributes
data: T[];
columns: Column[];
initialState?: TableState;
+ parentClass?: string;
row_height?: number;
getCustomCellProps?: (cell: Cell) => object;
}
@@ -17,6 +18,7 @@ const Table = ({
columns,
initialState,
getCustomCellProps = defaultPropGetter,
+ parentClass,
row_height,
...rest
}: ITableProps) => {
@@ -27,19 +29,28 @@ const Table = ({
});
return (
-
-
+
+
{headerGroups.map((headerGroup) => (
-
+
{headerGroup.headers.map((column) => (
-
+ | 1000 ? 'auto' : column.maxWidth,
+ }}
+ >
{column.render('Header')}
|
))}
))}
-
-
{rows.map((row) => {
prepareRow(row);
return (
@@ -50,7 +61,14 @@ const Table = ({
>
{row.cells.map((cell) => {
return (
-
+ | 1000 ? 'auto' : cell.column.maxWidth,
+ }}
+ >
{cell.render('Cell', getCustomCellProps(cell))}
|
);
diff --git a/src/features/dashboard/components/Table/scopes.cell.module.scss b/src/features/dashboard/components/Table/scopes.cell.module.scss
index 57dfa80e9..f894ac0e9 100644
--- a/src/features/dashboard/components/Table/scopes.cell.module.scss
+++ b/src/features/dashboard/components/Table/scopes.cell.module.scss
@@ -3,7 +3,7 @@
.scope {
display: inline-block;
border: rem(0.1) solid var(--ifm-color-emphasis-400);
- border-radius: 100vw; // pill shaped
+ border-radius: 4px;
padding: rem(0.2) rem(0.8);
font-size: rem(1.2);
margin: rem(0.5);
diff --git a/src/features/dashboard/components/Table/scopes.cell.tsx b/src/features/dashboard/components/Table/scopes.cell.tsx
index af249b858..03abcf98b 100644
--- a/src/features/dashboard/components/Table/scopes.cell.tsx
+++ b/src/features/dashboard/components/Table/scopes.cell.tsx
@@ -1,13 +1,21 @@
import React from 'react';
-import { CellProps } from 'react-table';
import styles from './scopes.cell.module.scss';
-const ScopesCell = ({
- cell,
-}: React.PropsWithChildren>) => {
- return (
- <>
- {cell.value.map((scopes: string): React.ReactElement => {
+type TScopesCellProps = {
+ cell: {
+ value: string[];
+ };
+};
+
+const SCOPES_ORDER = ['admin', 'read', 'payments', 'trade', 'trading_information'];
+
+const ScopesCell: React.FC = ({ cell }) => (
+ <>
+ {cell.value
+ .sort((a, b) => {
+ return SCOPES_ORDER.indexOf(a) - SCOPES_ORDER.indexOf(b);
+ })
+ .map((scopes: string): React.ReactElement => {
return (
({
);
})}
- >
- );
-};
+ >
+);
export default ScopesCell;
diff --git a/src/features/dashboard/index.tsx b/src/features/dashboard/index.tsx
index 637831f54..4d5cb6363 100644
--- a/src/features/dashboard/index.tsx
+++ b/src/features/dashboard/index.tsx
@@ -1,12 +1,13 @@
import React, { useEffect } from 'react';
-import { Login } from '../Auth/Login/Login';
import useAuthContext from '@site/src/hooks/useAuthContext';
-import DashboardTabs from './components/Tabs';
+// import DashboardTabs from './components/Tabs';
import useAppManager from '@site/src/hooks/useAppManager';
+import MemoizedManageDashboard from './manage-dashboard';
+import { Login } from '../Auth/Login/Login';
export const AppManager = () => {
const { is_logged_in } = useAuthContext();
- const { setIsDashboard, is_dashboard } = useAppManager();
+ const { setIsDashboard } = useAppManager();
useEffect(() => {
setIsDashboard(true);
@@ -15,5 +16,5 @@ export const AppManager = () => {
};
}, [setIsDashboard]);
- return {is_logged_in ? : };
+ return {is_logged_in ? : };
};
diff --git a/src/features/dashboard/manage-apps/app-manage-page.tsx b/src/features/dashboard/manage-apps/app-manage-page.tsx
new file mode 100644
index 000000000..86fa52ab2
--- /dev/null
+++ b/src/features/dashboard/manage-apps/app-manage-page.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+import AppsTable from '../components/AppsTable';
+import LoadingTable from '../components/LoadingTable';
+import { ApplicationObject } from '@deriv/api-types';
+
+const AppManagePage: React.FC<{ apps: ApplicationObject[] }> = ({ apps }) => {
+ return (
+
+ );
+};
+
+export default AppManagePage;
diff --git a/src/features/dashboard/manage-apps/index.tsx b/src/features/dashboard/manage-apps/index.tsx
index 57575d4c7..a71e37055 100644
--- a/src/features/dashboard/manage-apps/index.tsx
+++ b/src/features/dashboard/manage-apps/index.tsx
@@ -1,8 +1,8 @@
import useAppManager from '@site/src/hooks/useAppManager';
import React, { useEffect } from 'react';
-import AppsTable from '../components/AppsTable';
-import LoadingTable from '../components/LoadingTable';
-import styles from './manage-apps.module.scss';
+import AppManagePage from './app-manage-page';
+import CustomTabs from '@site/src/components/CustomTabs';
+import './manage-apps.scss';
const AppManagement = () => {
const { getApps, apps } = useAppManager();
@@ -11,9 +11,17 @@ const AppManagement = () => {
getApps();
}, [getApps]);
+ const tabs = [
+ {
+ label: 'Applications',
+ content: ,
+ },
+ { label: 'API tokens', content: API tokens development in progress
},
+ ];
+
return (
-
- {apps ?
:
}
+
+
);
};
diff --git a/src/features/dashboard/manage-apps/manage-apps.module.scss b/src/features/dashboard/manage-apps/manage-apps.scss
similarity index 64%
rename from src/features/dashboard/manage-apps/manage-apps.module.scss
rename to src/features/dashboard/manage-apps/manage-apps.scss
index b2beab871..84915f88d 100644
--- a/src/features/dashboard/manage-apps/manage-apps.module.scss
+++ b/src/features/dashboard/manage-apps/manage-apps.scss
@@ -1,10 +1,8 @@
@use 'src/styles/utility' as *;
-.manageApps {
+.manage_apps {
width: 100%;
- display: inline-block;
overflow: auto;
- max-height: calc(100vh - rem(35));
border-top-left-radius: rem(1.6);
border-top-right-radius: rem(1.6);
-}
\ No newline at end of file
+}
diff --git a/src/features/dashboard/manage-dashboard/__tests__/manage-dashboard.test.tsx b/src/features/dashboard/manage-dashboard/__tests__/manage-dashboard.test.tsx
new file mode 100644
index 000000000..ecfbf1e9b
--- /dev/null
+++ b/src/features/dashboard/manage-dashboard/__tests__/manage-dashboard.test.tsx
@@ -0,0 +1,163 @@
+import React from 'react';
+import { cleanup, render, screen } from '@site/src/test-utils';
+import MemoizedManageDashboard from '..';
+import useAppManager from '@site/src/hooks/useAppManager';
+import useDeviceType from '@site/src/hooks/useDeviceType';
+import userEvent from '@testing-library/user-event';
+import apiManager from '@site/src/configs/websocket';
+
+jest.mock('@site/src/hooks/useAppManager');
+const mockUseAppManager = useAppManager as jest.MockedFunction<
+ () => Partial
>
+>;
+mockUseAppManager.mockImplementation(() => ({
+ getApps: jest.fn(),
+ apps: undefined,
+ tokens: undefined,
+ updateCurrentTab: jest.fn(),
+}));
+
+jest.mock('@site/src/hooks/useDeviceType');
+const mockDeviceType = useDeviceType as jest.MockedFunction<
+ () => Partial>
+>;
+mockDeviceType.mockImplementation(() => ({
+ deviceType: 'desktop',
+}));
+
+jest.mock('@site/src/configs/websocket');
+const mockApiManager = apiManager as jest.Mocked;
+
+describe('ManageDashboard', () => {
+ afterEach(() => {
+ cleanup();
+ jest.clearAllMocks();
+ });
+
+ it('Should render the initial compoent with loader', () => {
+ const { container } = render();
+ expect(container).toBeInTheDocument();
+ const loader = screen.getByTestId('dt_spinner');
+ expect(loader).toBeInTheDocument();
+ });
+
+ it('Should render the content App Register page in mobile device - if no token or app is available', () => {
+ mockUseAppManager.mockImplementation(() => ({
+ apps: [],
+ tokens: [],
+ getApps: jest.fn(),
+ updateCurrentTab: jest.fn(),
+ }));
+ mockDeviceType.mockImplementation(() => ({
+ deviceType: 'mobile',
+ }));
+ render();
+ const register_button = screen.getByText(/Register now/i);
+ expect(register_button).toBeInTheDocument();
+ });
+
+ it('Should call getApps on submit button press if all the fields are filled up', async () => {
+ const mockGetApps = jest.fn();
+ mockUseAppManager.mockImplementation(() => ({
+ apps: [],
+ tokens: [],
+ getApps: mockGetApps,
+ updateCurrentTab: jest.fn(),
+ }));
+ render();
+
+ const name_input = screen.getByRole('textbox');
+ await userEvent.type(name_input, 'test create token');
+ const tnc_input = screen.getByRole('checkbox');
+ await userEvent.click(tnc_input);
+ const register_button = screen.getByText(/Register now/i);
+ await userEvent.click(register_button);
+
+ expect(mockGetApps).toHaveBeenCalled();
+ });
+
+ it('Should trigger the success modal in desktop', async () => {
+ const mockModalOpenSetter = jest.fn();
+ mockApiManager.augmentedSend.mockResolvedValue({
+ app_register: {
+ active: 1,
+ app_id: 1234,
+ app_markup_percentage: 0,
+ appstore: '',
+ github: '',
+ googleplay: '',
+ homepage: '',
+ name: 'TestApp1',
+ redirect_uri: '',
+ scopes: [],
+ verification_uri: '',
+ },
+ echo_req: {
+ app_markup_percentage: 0,
+ app_register: 1,
+ name: 'TestApp1',
+ req_id: 4,
+ scopes: [],
+ },
+ msg_type: 'app_register',
+ req_id: 4,
+ });
+
+ mockUseAppManager.mockImplementation(() => ({
+ getApps: jest.fn(),
+ apps: [],
+ tokens: [],
+ setAppRegisterModalOpen: mockModalOpenSetter,
+ updateCurrentTab: jest.fn(),
+ }));
+
+ render();
+
+ const name_input = screen.getByRole('textbox');
+ await userEvent.type(name_input, 'test create token');
+ const tnc_input = screen.getByRole('checkbox');
+ await userEvent.click(tnc_input);
+ const register_button = screen.getByText(/Register now/i);
+ await userEvent.click(register_button);
+
+ expect(mockModalOpenSetter).toBeCalledWith(true);
+ });
+
+ it('Should close the modal on config button click', async () => {
+ const mockModalOpenSetter = jest.fn();
+ mockUseAppManager.mockImplementation(() => ({
+ getApps: jest.fn(),
+ apps: [],
+ tokens: [],
+ setAppRegisterModalOpen: mockModalOpenSetter,
+ app_register_modal_open: true,
+ updateCurrentTab: jest.fn(),
+ }));
+
+ render();
+
+ const config_button = screen.getByText(/Config/i);
+ await userEvent.click(config_button);
+
+ expect(mockModalOpenSetter).toBeCalledWith(false);
+ });
+
+ it('Should close the modal on cancel button click', async () => {
+ const mockModalOpenSetter = jest.fn();
+ mockUseAppManager.mockImplementation(() => ({
+ getApps: jest.fn(),
+ apps: [],
+ tokens: [],
+ setAppRegisterModalOpen: mockModalOpenSetter,
+ app_register_modal_open: true,
+ updateCurrentTab: jest.fn(),
+ }));
+
+ render();
+
+ const cancel_button = screen.getByText(/Maybe Later/i);
+ await userEvent.click(cancel_button);
+
+ expect(mockModalOpenSetter).toBeCalledWith(false);
+ });
+});
diff --git a/src/features/dashboard/manage-dashboard/index.tsx b/src/features/dashboard/manage-dashboard/index.tsx
new file mode 100644
index 000000000..915ddff18
--- /dev/null
+++ b/src/features/dashboard/manage-dashboard/index.tsx
@@ -0,0 +1,89 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import AppDashboardContainer from '../components/AppDashboardContainer';
+import AppRegister from '../components/AppRegister';
+import useAppManager from '@site/src/hooks/useAppManager';
+import useApiToken from '@site/src/hooks/useApiToken';
+import Spinner from '@site/src/components/Spinner';
+import useWS from '@site/src/hooks/useWs';
+import useDeviceType from '@site/src/hooks/useDeviceType';
+import { RegisterAppDialogError } from '../components/Dialogs/RegisterAppDialogError';
+import { AppRegisterSuccessModal } from '../components/Modals/AppRegisterSuccessModal';
+import AppManagement from '../manage-apps';
+import './manage-dashboard.scss';
+
+const ManageDashboard = () => {
+ const { apps, getApps, setAppRegisterModalOpen, currentTab, updateCurrentTab } = useAppManager();
+ const { tokens } = useApiToken();
+ const { send: registerApp, error, clear, data, is_loading } = useWS('app_register');
+ const { deviceType } = useDeviceType();
+ const [is_desktop, setIsDesktop] = useState(true);
+
+ useEffect(() => {
+ setIsDesktop(deviceType.includes('desktop'));
+ }, [deviceType]);
+
+ useEffect(() => {
+ if (!is_loading && data?.name && !error) {
+ setAppRegisterModalOpen(true);
+ clear();
+ getApps();
+ }
+ }, [data, clear, error, setAppRegisterModalOpen, is_loading, getApps]);
+
+ useEffect(() => {
+ getApps();
+ }, [getApps]);
+
+ useEffect(() => {
+ if (!apps?.length && !tokens?.length) {
+ updateCurrentTab('REGISTER_APP');
+ } else {
+ updateCurrentTab('MANAGE_APPS');
+ }
+ }, [tokens, apps, updateCurrentTab]);
+
+ const submit = useCallback(
+ (data) => {
+ const { name } = data;
+ registerApp({
+ name,
+ scopes: [],
+ });
+ },
+ [registerApp],
+ );
+
+ if (!apps || is_loading || !tokens)
+ return (
+
+
+
+ );
+
+ const renderScreen = () => {
+ switch (currentTab) {
+ case 'REGISTER_APP':
+ return ;
+ case 'MANAGE_APPS':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+ {error && }
+ setAppRegisterModalOpen(false)}
+ onConfigure={() => setAppRegisterModalOpen(false)}
+ />
+ {renderScreen()}
+
+ );
+};
+
+const MemoizedManageDashboard = React.memo(ManageDashboard);
+
+export default MemoizedManageDashboard;
diff --git a/src/features/dashboard/manage-dashboard/manage-dashboard.scss b/src/features/dashboard/manage-dashboard/manage-dashboard.scss
new file mode 100644
index 000000000..a9d7e8a5d
--- /dev/null
+++ b/src/features/dashboard/manage-dashboard/manage-dashboard.scss
@@ -0,0 +1,5 @@
+.manage_dashboard {
+ &__spinner {
+ height: 90vh;
+ }
+}
diff --git a/src/hooks/useAppManager/__tests__/useAppManager.test.tsx b/src/hooks/useAppManager/__tests__/useAppManager.test.tsx
index a16f6ba6a..ab470ddb2 100644
--- a/src/hooks/useAppManager/__tests__/useAppManager.test.tsx
+++ b/src/hooks/useAppManager/__tests__/useAppManager.test.tsx
@@ -50,9 +50,9 @@ describe('use App Manager', () => {
await expect(wsServer).toReceiveMessage({ app_list: 1, req_id: 1 });
});
- it('Should have MANAGE_TOKENS as initial value for currentTab', () => {
+ it('Should have MANAGE_APPS as initial value for currentTab', () => {
const { result } = renderHook(() => useAppManager(), { wrapper });
- expect(result.current.currentTab).toBe('MANAGE_TOKENS');
+ expect(result.current.currentTab).toBe('MANAGE_APPS');
});
it('Should update currentTab value', () => {
diff --git a/src/hooks/useDeviceType/index.tsx b/src/hooks/useDeviceType/index.tsx
new file mode 100644
index 000000000..332ba2e14
--- /dev/null
+++ b/src/hooks/useDeviceType/index.tsx
@@ -0,0 +1,33 @@
+import { useState, useEffect } from 'react';
+import { debounceTime, fromEvent } from 'rxjs';
+
+type TDeviceType = 'mobile' | 'tablet' | 'desktop';
+
+type TUseDeviceType = {
+ deviceType: TDeviceType;
+};
+
+const useDeviceType = (): TUseDeviceType => {
+ const [deviceType, setDeviceType] = useState('desktop');
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.matchMedia('(max-width: 768px)').matches) {
+ setDeviceType('mobile');
+ } else if (window.matchMedia('(max-width: 1023px)').matches) {
+ setDeviceType('tablet');
+ } else {
+ setDeviceType('desktop');
+ }
+ };
+
+ handleResize();
+ const resize = fromEvent(window, 'resize');
+ const result = resize.pipe(debounceTime(600));
+ result.subscribe(handleResize);
+ }, []);
+
+ return { deviceType };
+};
+
+export default useDeviceType;
diff --git a/src/styles/index.scss b/src/styles/index.scss
index b5b8ec452..535b94543 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -1,5 +1,6 @@
@use 'src/styles/utility' as *;
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;700&family=Ubuntu:wght@400;500;700&display=swap');
+@import '@deriv/quill-design/dist/quill-design.css';
/**
* Any CSS included here will be global. The classic template
@@ -40,6 +41,10 @@
--smoke: #414652;
--admin-text: #22bd41;
--admin-border: #33c9517a;
+ --solid-slate-50: #ffffff;
+ --solid-slate-75: #f6f7f8;
+ --opacity-black-100: #00000014;
+ --opacity-black-75: #0000000a;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
diff --git a/static/img/circle_check_regular_icon.svg b/static/img/circle_check_regular_icon.svg
new file mode 100644
index 000000000..004b7a8ae
--- /dev/null
+++ b/static/img/circle_check_regular_icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/img/circle_dot_caption_bold.svg b/static/img/circle_dot_caption_bold.svg
new file mode 100644
index 000000000..986d59044
--- /dev/null
+++ b/static/img/circle_dot_caption_bold.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/img/circle_dot_caption_fill.svg b/static/img/circle_dot_caption_fill.svg
new file mode 100644
index 000000000..4c4f0a128
--- /dev/null
+++ b/static/img/circle_dot_caption_fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file