1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

[MMI] confirm-add-custodian-token component (#18261)

* adds component with locales and test

* adds feedback from review

* adds storeis file

* update snapshots

* prettier

* clean up stories file

* relocation

* review fixes

* Update ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Update ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Update ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* Button path remove

* Update ui/pages/institutional/confirm-add-custodian-token/confirm-add-custodian-token.js

Co-authored-by: George Marshall <george.marshall@consensys.net>

* pulled

* stories file update location

---------

Co-authored-by: George Marshall <george.marshall@consensys.net>
This commit is contained in:
António Regadas 2023-03-31 10:30:10 +01:00 committed by GitHub
parent e895ff33f9
commit 234fb4ac5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 501 additions and 46 deletions

View File

@ -103,6 +103,9 @@
"SIWEWarningTitle": {
"message": "Are you sure?"
},
"ShowMore": {
"message": "Show more"
},
"about": {
"message": "About"
},
@ -348,6 +351,9 @@
"amount": {
"message": "Amount"
},
"apiUrl": {
"message": "API URL"
},
"appDescription": {
"message": "An Ethereum Wallet in your Browser",
"description": "The description of the application"
@ -908,6 +914,12 @@
"curveMediumGasEstimate": {
"message": "Market gas estimate graph"
},
"custodian": {
"message": "Custodian"
},
"custodianAccount": {
"message": "Custodian account"
},
"custom": {
"message": "Advanced"
},
@ -2118,6 +2130,9 @@
"missingToken": {
"message": "Don't see your token?"
},
"mmiAddToken": {
"message": "The page at $1 would like to authorise the following custodian token in MetaMask Institutional"
},
"mobileSyncWarning": {
"message": "The 'Sync with extension' feature is temporarily disabled. If you want to use your extension wallet on MetaMask mobile, then on your mobile app: go back to the wallet setup options and select the 'Import with Secret Recovery Phrase' option. Use your extension wallet's secret phrase to then import your wallet into mobile."
},

View File

@ -3,16 +3,16 @@
exports[`JwtUrlForm shows JWT text area when no jwt token exists 1`] = `
<div>
<div
class="box jwt-url-form box--margin-bottom-8 box--display-flex box--flex-direction-row box--align-items-flex-start"
class="box jwt-url-form box--margin-bottom-8 box--display-flex box--flex-direction-column box--align-items-flex-start"
>
<div
class="box jwt-url-form__jwt-container box--margin-bottom-6 box--flex-direction-row"
class="box jwt-url-form__jwt-container box--margin-top-4 box--margin-bottom-6 box--display-flex box--flex-direction-column box--align-items-center box--width-full"
>
<div
class="box box--flex-direction-row"
>
<p
class="box mm-text jwt-url-form__instruction mm-text--body-md box--flex-direction-row box--color-text-default"
class="box mm-text jwt-url-form__instruction mm-text--body-md box--display-block box--flex-direction-row box--color-text-default"
>
input text
</p>
@ -25,10 +25,10 @@ exports[`JwtUrlForm shows JWT text area when no jwt token exists 1`] = `
</div>
</div>
<div
class="box jwt-url-form__jwt-apiUrlInput box--flex-direction-row"
class="box box--flex-direction-row box--width-full"
>
<p
class="box mm-text jwt-url-form__instruction mm-text--body-md box--flex-direction-row box--color-text-default"
class="box mm-text jwt-url-form__instruction mm-text--body-md box--display-block box--flex-direction-row box--color-text-default"
/>
<div
class="box box--flex-direction-row"

View File

@ -5,6 +5,8 @@ import {
AlignItems,
DISPLAY,
BorderColor,
BLOCK_SIZES,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
import { Text } from '../../component-library';
import JwtDropdown from '../jwt-dropdown';
@ -22,7 +24,15 @@ const JwtUrlForm = (props) => {
const showJwtDropdown = props.jwtList.length >= 1;
return (
<Box className="jwt-url-form__jwt-container" marginBottom={6}>
<Box
width={BLOCK_SIZES.FULL}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
marginTop={4}
alignItems={AlignItems.center}
className="jwt-url-form__jwt-container"
marginBottom={6}
>
{showJwtDropdown && (
<JwtDropdown
data-testid="jwt-dropdown"
@ -36,13 +46,15 @@ const JwtUrlForm = (props) => {
)}
{showJwtDropdown && !showAddNewToken && (
<Box
className="jwt-url-form__btn__container"
width={BLOCK_SIZES.FULL}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.center}
marginTop={2}
>
<Text>{t('or')}</Text>
<Button
data-testid="addNewToken-btn"
type="secondary"
medium="true"
onClick={() => {
@ -56,7 +68,7 @@ const JwtUrlForm = (props) => {
)}
{(!showJwtDropdown || showAddNewToken) && (
<Box>
<Text className="jwt-url-form__instruction">
<Text className="jwt-url-form__instruction" display={DISPLAY.BLOCK}>
{props.jwtInputText}
</Text>
{fileTooBigError && (
@ -85,8 +97,10 @@ const JwtUrlForm = (props) => {
const renderAPIURLInput = () => {
return (
<Box className="jwt-url-form__jwt-apiUrlInput">
<Text className="jwt-url-form__instruction">{props.urlInputText}</Text>
<Box width={BLOCK_SIZES.FULL}>
<Text className="jwt-url-form__instruction" display={DISPLAY.BLOCK}>
{props.urlInputText}
</Text>
<Box>
<input
className="jwt-url-form__input"
@ -106,6 +120,7 @@ const JwtUrlForm = (props) => {
<Box
className="jwt-url-form"
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.flexStart}
marginBottom={8}
>

View File

@ -1,28 +1,8 @@
.jwt-url-form {
flex-flow: column;
&__jwt-container {
width: 100%;
}
&__jwt-apiUrlInput {
width: 100%;
}
&__btn__container {
flex-flow: column;
width: 100%;
& > span {
padding-bottom: 10px;
}
}
&__instruction {
@include Paragraph;
align-self: flex-start;
display: block;
font-size: 14px;
}
@ -37,14 +17,6 @@
padding: 0 10px;
}
&__input-jwt-container {
display: flex;
flex-flow: column;
align-items: center;
width: 100%;
margin-top: 16px;
}
&__input-jwt {
@include Paragraph;

View File

@ -27,24 +27,25 @@ describe('JwtUrlForm', function () {
};
it('opens JWT Url Form without input for new JWT', () => {
const { container, getByText } = renderWithProvider(
const { getAllByTestId, getByText } = renderWithProvider(
<JwtUrlForm {...props} />,
store,
);
const btn = container.querySelector(
'.jwt-url-form__btn__container .btn-secondary',
expect(getAllByTestId('addNewToken-btn')[0]).toHaveAttribute(
'role',
'button',
);
expect(btn).toHaveClass('button');
expect(getByText('Add new token')).toBeInTheDocument();
});
it('shows JWT textarea with provided input text', () => {
const { container } = renderWithProvider(<JwtUrlForm {...props} />, store);
const btn = container.querySelector(
'.jwt-url-form__btn__container .btn-secondary',
const { getAllByTestId } = renderWithProvider(
<JwtUrlForm {...props} />,
store,
);
const btn = getAllByTestId('addNewToken-btn')[0];
fireEvent.click(btn);
expect(screen.getByText('input text')).toBeInTheDocument();
});

View File

@ -20,6 +20,9 @@ const CONTACT_LIST_ROUTE = '/settings/contact-list';
const CONTACT_EDIT_ROUTE = '/settings/contact-list/edit-contact';
const CONTACT_ADD_ROUTE = '/settings/contact-list/add-contact';
const CONTACT_VIEW_ROUTE = '/settings/contact-list/view-contact';
///: BEGIN:ONLY_INCLUDE_IN(mmi)
const CUSTODY_ACCOUNT_ROUTE = '/new-account/custody';
///: END:ONLY_INCLUDE_IN
const REVEAL_SEED_ROUTE = '/seed';
const MOBILE_SYNC_ROUTE = '/mobile-sync';
const RESTORE_VAULT_ROUTE = '/restore-vault';
@ -203,6 +206,9 @@ export {
CONTACT_EDIT_ROUTE,
CONTACT_ADD_ROUTE,
CONTACT_VIEW_ROUTE,
///: BEGIN:ONLY_INCLUDE_IN(mmi)
CUSTODY_ACCOUNT_ROUTE,
///: END:ONLY_INCLUDE_IN
NETWORKS_ROUTE,
NETWORKS_FORM_ROUTE,
ADD_NETWORK_ROUTE,

View File

@ -0,0 +1,268 @@
import React, { useContext, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import PulseLoader from '../../../components/ui/pulse-loader';
import { CUSTODY_ACCOUNT_ROUTE } from '../../../helpers/constants/routes';
import {
AlignItems,
DISPLAY,
TextColor,
TEXT_ALIGN,
FLEX_DIRECTION,
} from '../../../helpers/constants/design-system';
import { BUILT_IN_NETWORKS } from '../../../../shared/constants/network';
import { I18nContext } from '../../../contexts/i18n';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { setProviderType } from '../../../store/actions';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
import {
Label,
Text,
ButtonLink,
Button,
BUTTON_SIZES,
BUTTON_TYPES,
} from '../../../components/component-library';
import Box from '../../../components/ui/box';
const ConfirmAddCustodianToken = () => {
const t = useContext(I18nContext);
const dispatch = useDispatch();
const history = useHistory();
const trackEvent = useContext(MetaMetricsContext);
const mmiActions = mmiActionsFactory();
const mostRecentOverviewPage = useSelector(getMostRecentOverviewPage);
const connectRequests = useSelector(
(state) => state.metamask.institutionalFeatures?.connectRequests,
);
const complianceActivated = useSelector((state) =>
Boolean(state.metamask.institutionalFeatures?.complianceProjectId),
);
const [showMore, setShowMore] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [connectError, setConnectError] = useState('');
const handleConnectError = (e) => {
let errorMessage = e.message;
if (!errorMessage) {
errorMessage = 'Connection error';
}
setConnectError(errorMessage);
setIsLoading(false);
};
const renderSelectedToken = () => {
const connectRequest = connectRequests ? connectRequests[0] : undefined;
return (
<Box
paddingTop={2}
paddingBottom={2}
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.ROW}
alignItems={AlignItems.center}
>
<Text>
{showMore && connectRequest?.token
? connectRequest?.token
: `...${connectRequest?.token.slice(-9)}`}
</Text>
{!showMore && (
<Box paddingLeft={2}>
<ButtonLink
rel="noopener noreferrer"
onClick={() => {
setShowMore(true);
}}
>
{t('ShowMore')}
</ButtonLink>
</Box>
)}
</Box>
);
};
const connectRequest = connectRequests ? connectRequests[0] : undefined;
if (!connectRequest) {
history.push(mostRecentOverviewPage);
return null;
}
trackEvent({
category: 'MMI',
event: 'Custodian onboarding',
properties: {
actions: 'Custodian RPC request',
custodian: connectRequest.custodian,
apiUrl: connectRequest.apiUrl,
},
});
let custodianLabel = '';
if (
connectRequest.labels &&
connectRequest.labels.some((label) => label.key === 'service')
) {
custodianLabel = connectRequest.labels.find(
(label) => label.key === 'service',
).value;
}
return (
<Box className="page-container">
<Box className="page-container__header">
<Text className="page-container__title">{t('custodianAccount')}</Text>
<Text className="page-container__subtitle">
{t('mmiAddToken', [connectRequest.origin])}
</Text>
</Box>
<Box padding={4} className="page-container__content">
{custodianLabel && (
<>
<Text padding={4} color={TextColor.textDefault}>
{t('custodian')}
</Text>
<Label
marginRight={4}
marginLeft={4}
color={TextColor.textAlternative}
className="add_custodian_token_confirm__url"
>
{custodianLabel}
</Label>
</>
)}
<Text padding={4} color={TextColor.textDefault}>
{t('token')}
</Text>
<Box
marginRight={4}
marginLeft={4}
className="add_custodian_token_confirm__token"
>
{renderSelectedToken()}
</Box>
{connectRequest.apiUrl && (
<Box>
<Text padding={4} color={TextColor.textDefault}>
{t('apiUrl')}
</Text>
<Text
marginRight={4}
marginLeft={4}
color={TextColor.textAlternative}
fontSize="14"
className="add_custodian_token_confirm__url"
>
{connectRequest.apiUrl}
</Text>
</Box>
)}
</Box>
{!complianceActivated && (
<Box marginTop={4} data-testid="connect-custodian-token-error">
<Text data-testid="error-message" textAlign={TEXT_ALIGN.CENTER}>
{connectError}
</Text>
</Box>
)}
<Box as="footer" className="page-container__footer" padding={4}>
{isLoading ? (
<PulseLoader />
) : (
<Box display={DISPLAY.FLEX} gap={4}>
<Button
block
type={BUTTON_TYPES.SECONDARY}
size={BUTTON_SIZES.LG}
data-testid="cancel-btn"
onClick={() => {
mmiActions.removeAddTokenConnectRequest({
origin: connectRequest.origin,
apiUrl: connectRequest.apiUrl,
token: connectRequest.token,
});
history.push(mostRecentOverviewPage);
trackEvent({
category: 'MMI',
event: 'Custodian onboarding',
properties: {
actions: 'Custodian RPC cancel',
custodian: connectRequest.custodian,
apiUrl: connectRequest.apiUrl,
},
});
}}
>
{t('cancel')}
</Button>
<Button
block
data-testid="confirm-btn"
size={BUTTON_SIZES.LG}
onClick={async () => {
setConnectError('');
setIsLoading(true);
try {
if (connectRequest.chainId) {
const networkType = Object.keys(BUILT_IN_NETWORKS).find(
(key) =>
Number(BUILT_IN_NETWORKS[key].chainId).toString(10) ===
connectRequest.chainId.toString(),
);
await dispatch(setProviderType(networkType));
}
let custodianName = connectRequest.service.toLowerCase();
if (connectRequest.service === 'JSONRPC') {
custodianName = connectRequest.environment;
}
await mmiActions.setCustodianConnectRequest({
token: connectRequest.token,
apiUrl: connectRequest.apiUrl,
custodianName,
custodianType: connectRequest.service,
});
mmiActions.removeAddTokenConnectRequest({
origin: connectRequest.origin,
apiUrl: connectRequest.apiUrl,
token: connectRequest.token,
});
trackEvent({
category: 'MMI',
event: 'Custodian onboarding',
properties: {
actions: 'Custodian RPC confirm',
custodian: connectRequest.custodian,
apiUrl: connectRequest.apiUrl,
},
});
history.push(CUSTODY_ACCOUNT_ROUTE);
} catch (e) {
handleConnectError(e);
}
}}
>
{t('confirm')}
</Button>
</Box>
)}
</Box>
</Box>
);
};
export default ConfirmAddCustodianToken;

View File

@ -0,0 +1,43 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
import testData from '../../../../.storybook/test-data';
import ConfirmAddCustodianToken from '.';
const customData = {
...testData,
metamask: {
...testData.metamask,
institutionalFeatures: {
complianceProjectId: '',
connectRequests: [
{
labels: [
{
key: 'service',
value: 'test',
},
],
origin: 'origin',
token: 'awesomeTestToken',
feature: 'custodian',
service: 'Saturn',
apiUrl: 'https://www.apiurl.net/v1',
chainId: 1,
},
],
},
},
};
const store = configureStore(customData);
export default {
title: 'Pages/Institutional/ConfirmAddCustodianToken',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: ConfirmAddCustodianToken,
};
export const DefaultStory = () => <ConfirmAddCustodianToken />;
DefaultStory.storyName = 'ConfirmAddCustodianToken';

View File

@ -0,0 +1,126 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import ConfirmAddCustodianToken from './confirm-add-custodian-token';
describe('Confirm Add Custodian Token', () => {
const mockStore = {
metamask: {
provider: {
type: 'test',
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
institutionalFeatures: {
complianceProjectId: '',
connectRequests: [
{
labels: [
{
key: 'service',
value: 'test',
},
],
origin: 'origin',
token: 'testToken',
feature: 'custodian',
service: 'Jupiter',
apiUrl: 'https://',
chainId: 1,
},
],
},
},
history: {
push: '/',
mostRecentOverviewPage: '/',
},
};
const store = configureMockStore()(mockStore);
it('opens confirm add custodian token with correct token', () => {
renderWithProvider(<ConfirmAddCustodianToken />, store);
const tokenContainer = screen.getByText('...testToken');
expect(tokenContainer).toBeInTheDocument();
});
it('shows the custodian on cancel click', () => {
renderWithProvider(<ConfirmAddCustodianToken />, store);
const cancelButton = screen.getByTestId('cancel-btn');
fireEvent.click(cancelButton);
expect(screen.getByText('Custodian')).toBeInTheDocument();
});
it('tries to connect to custodian with empty token', async () => {
const customMockedStore = {
metamask: {
provider: {
type: 'test',
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
institutionalFeatures: {
complianceProjectId: '',
connectRequests: [
{
labels: [
{
key: 'service',
value: 'test',
},
],
origin: 'origin',
token: '',
feature: 'custodian',
service: 'Jupiter',
apiUrl: 'https://',
chainId: 1,
},
],
},
},
history: {
push: '/',
mostRecentOverviewPage: '/',
},
};
const customStore = configureMockStore()(customMockedStore);
renderWithProvider(<ConfirmAddCustodianToken />, customStore);
const confirmButton = screen.getByTestId('confirm-btn');
fireEvent.click(confirmButton);
const errorMessage = screen.getByTestId('connect-custodian-token-error');
expect(errorMessage).toBeVisible();
});
it('clicks the confirm button and shows the test value', async () => {
renderWithProvider(<ConfirmAddCustodianToken />, store);
const confirmButton = screen.getByTestId('confirm-btn');
fireEvent.click(confirmButton);
expect(screen.getByText('test')).toBeInTheDocument();
});
it('shows the error area', () => {
renderWithProvider(<ConfirmAddCustodianToken />, store);
const confirmButton = screen.getByTestId('confirm-btn');
fireEvent.click(confirmButton);
expect(screen.getByTestId('error-message')).toBeVisible();
});
});

View File

@ -0,0 +1 @@
export { default } from './confirm-add-custodian-token';

View File

@ -0,0 +1,8 @@
.add_custodian_token_confirm {
&__token,
&__url {
height: auto;
overflow-wrap: break-word;
opacity: 0.8;
}
}