diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 733e0edc8..eefe50cb5 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -257,6 +257,9 @@
"message": "This network connection relies on third parties. This connection may be less reliable or enable third-parties to track activity. $1",
"description": "$1 is Learn more link"
},
+ "addNewToken": {
+ "message": "Add new token"
+ },
"addSuggestedTokens": {
"message": "Add suggested tokens"
},
@@ -1370,6 +1373,9 @@
"message": "File import not working? Click here!",
"description": "Helps user import their account from a JSON file"
},
+ "fileTooBig": {
+ "message": "The dropped file is too big."
+ },
"flaskSnapSettingsCardButtonCta": {
"message": "See details",
"description": "Call to action a user can take to see more information about the snap that is installed"
@@ -3331,6 +3337,9 @@
"selectHdPath": {
"message": "Select HD path"
},
+ "selectJWT": {
+ "message": "Select token"
+ },
"selectNFTPrivacyPreference": {
"message": "Turn on NFT detection in Settings"
},
@@ -4003,7 +4012,7 @@
"message": "Select a quote"
},
"swapSelectAToken": {
- "message": "Select a token"
+ "message": "Select token"
},
"swapSelectQuotePopoverDescription": {
"message": "Below are all the quotes gathered from multiple liquidity sources."
diff --git a/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.js.snap b/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.js.snap
new file mode 100644
index 000000000..2d9c15b48
--- /dev/null
+++ b/ui/components/institutional/jwt-url-form/__snapshots__/jwt-url-form.test.js.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`JwtUrlForm shows JWT text area when no jwt token exists 1`] = `
+
+`;
diff --git a/ui/components/institutional/jwt-url-form/index.js b/ui/components/institutional/jwt-url-form/index.js
new file mode 100644
index 000000000..f3e07ae84
--- /dev/null
+++ b/ui/components/institutional/jwt-url-form/index.js
@@ -0,0 +1 @@
+export { default } from './jwt-url-form';
diff --git a/ui/components/institutional/jwt-url-form/jwt-url-form.js b/ui/components/institutional/jwt-url-form/jwt-url-form.js
new file mode 100644
index 000000000..c1c9f3edf
--- /dev/null
+++ b/ui/components/institutional/jwt-url-form/jwt-url-form.js
@@ -0,0 +1,128 @@
+import React, { useState, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ AlignItems,
+ DISPLAY,
+ BorderColor,
+} from '../../../helpers/constants/design-system';
+import { Text } from '../../component-library';
+import JwtDropdown from '../jwt-dropdown';
+import Button from '../../ui/button';
+import Box from '../../ui/box';
+
+const JwtUrlForm = (props) => {
+ const t = useI18nContext();
+ const inputRef = useRef();
+ const [addNewTokenClicked, setAddNewTokenClicked] = useState(false);
+ const [fileTooBigError, setFileTooBigError] = useState();
+
+ const renderJWTInput = () => {
+ const showAddNewToken = addNewTokenClicked;
+ const showJwtDropdown = props.jwtList.length >= 1;
+
+ return (
+
+ {showJwtDropdown && (
+ {
+ props.onJwtChange(value);
+ setFileTooBigError(false);
+ }}
+ />
+ )}
+ {showJwtDropdown && !showAddNewToken && (
+
+ {t('or')}
+ {
+ props.onJwtChange('');
+ setAddNewTokenClicked(true);
+ }}
+ >
+ {t('addNewToken')}
+
+
+ )}
+ {(!showJwtDropdown || showAddNewToken) && (
+
+
+ {props.jwtInputText}
+
+ {fileTooBigError && (
+
+ {t('fileTooBig')}
+
+ )}
+
+ )}
+
+ );
+ };
+
+ const renderAPIURLInput = () => {
+ return (
+
+ {props.urlInputText}
+
+ {
+ props.onUrlChange(e.target.value);
+ }}
+ value={props.apiUrl}
+ />
+
+
+ );
+ };
+
+ return (
+
+ {renderJWTInput()}
+ {renderAPIURLInput()}
+
+ );
+};
+
+JwtUrlForm.propTypes = {
+ jwtList: PropTypes.array,
+ currentJwt: PropTypes.string,
+ onJwtChange: PropTypes.func,
+ jwtInputText: PropTypes.string,
+ apiUrl: PropTypes.string,
+ urlInputText: PropTypes.string,
+ onUrlChange: PropTypes.func,
+};
+
+export default JwtUrlForm;
diff --git a/ui/components/institutional/jwt-url-form/jwt-url-form.scss b/ui/components/institutional/jwt-url-form/jwt-url-form.scss
new file mode 100644
index 000000000..cb235473d
--- /dev/null
+++ b/ui/components/institutional/jwt-url-form/jwt-url-form.scss
@@ -0,0 +1,58 @@
+.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;
+ }
+
+ &__input {
+ @include Paragraph;
+
+ height: 54px;
+ width: 100%;
+ border-radius: 4px;
+ background-color: var(--color-background-default);
+ margin-top: 16px;
+ padding: 0 10px;
+ }
+
+ &__input-jwt-container {
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ width: 100%;
+ margin-top: 16px;
+ }
+
+ &__input-jwt {
+ @include Paragraph;
+
+ height: 154px;
+ width: 100%;
+ border-radius: 4px;
+ background-color: var(--color-background-default);
+ color: var(--color-text-default);
+ padding: 10px;
+ }
+}
diff --git a/ui/components/institutional/jwt-url-form/jwt-url-form.stories.js b/ui/components/institutional/jwt-url-form/jwt-url-form.stories.js
new file mode 100644
index 000000000..0fee65dbe
--- /dev/null
+++ b/ui/components/institutional/jwt-url-form/jwt-url-form.stories.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import JwtUrlForm from '.';
+
+export default {
+ title: 'Components/Institutional/JwtUrlForm',
+ component: JwtUrlForm,
+ args: {
+ jwtList: ['jwt1', 'jwt2', 'jwt3'],
+ currentJwt: 'jwt1',
+ urlInputText: 'url',
+ apiUrl: 'https://apiurl.io/v1',
+ jwtInputText: 'some input text',
+ onJwtChange: () => {
+ /**/
+ },
+ onUrlChange: () => {
+ /**/
+ },
+ },
+};
+
+export const DefaultStory = (args) => ;
+
+DefaultStory.storyName = 'JwtUrlForm';
diff --git a/ui/components/institutional/jwt-url-form/jwt-url-form.test.js b/ui/components/institutional/jwt-url-form/jwt-url-form.test.js
new file mode 100644
index 000000000..5452b9fd9
--- /dev/null
+++ b/ui/components/institutional/jwt-url-form/jwt-url-form.test.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import sinon from 'sinon';
+import { fireEvent, screen } from '@testing-library/react';
+import configureMockStore from 'redux-mock-store';
+import { renderWithProvider } from '../../../../test/lib/render-helpers';
+import JwtUrlForm from './jwt-url-form';
+
+describe('JwtUrlForm', function () {
+ const mockStore = {
+ metamask: {
+ provider: {
+ type: 'test',
+ },
+ },
+ };
+
+ const store = configureMockStore()(mockStore);
+
+ const props = {
+ jwtList: ['jwt1'],
+ currentJwt: 'jwt1',
+ onJwtChange: sinon.spy(),
+ jwtInputText: 'input text',
+ apiUrl: 'url',
+ urlInputText: '',
+ onUrlChange: sinon.spy(),
+ };
+
+ it('opens JWT Url Form without input for new JWT', () => {
+ const { container, getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ const btn = container.querySelector(
+ '.jwt-url-form__btn__container .btn-secondary',
+ );
+ expect(btn).toHaveClass('button');
+ expect(getByText('Add new token')).toBeInTheDocument();
+ });
+
+ it('shows JWT textarea with provided input text', () => {
+ const { container } = renderWithProvider( , store);
+
+ const btn = container.querySelector(
+ '.jwt-url-form__btn__container .btn-secondary',
+ );
+ fireEvent.click(btn);
+ expect(screen.getByText('input text')).toBeInTheDocument();
+ });
+
+ it('goes through the api url input', () => {
+ const { queryByTestId } = renderWithProvider(
+ ,
+ store,
+ );
+
+ const apiUrlinput = queryByTestId('jwt-api-url-input');
+ fireEvent.change(apiUrlinput, { target: { value: 'url' } });
+ expect(apiUrlinput.value).toBe('url');
+ });
+
+ it('shows JWT text area when no jwt token exists', () => {
+ const customProps = {
+ ...props,
+ currentJwt: '',
+ jwtList: [],
+ };
+
+ const { container } = renderWithProvider(
+ ,
+ store,
+ );
+
+ expect(container).toMatchSnapshot();
+ });
+});