mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Approval flow adding success and error pages (#19778)
This commit is contained in:
parent
2fad10e0a3
commit
5736e670f7
12
app/_locales/en/messages.json
generated
12
app/_locales/en/messages.json
generated
@ -3504,6 +3504,18 @@
|
||||
"restoreUserDataDescription": {
|
||||
"message": "You can restore user settings containing preferences and account addresses from a previously backed up JSON file."
|
||||
},
|
||||
"resultPageError": {
|
||||
"message": "Error"
|
||||
},
|
||||
"resultPageErrorDefaultMessage": {
|
||||
"message": "The operation failed."
|
||||
},
|
||||
"resultPageSuccess": {
|
||||
"message": "Success"
|
||||
},
|
||||
"resultPageSuccessDefaultMessage": {
|
||||
"message": "The operation completed successfully."
|
||||
},
|
||||
"retryTransaction": {
|
||||
"message": "Retry transaction"
|
||||
},
|
||||
|
@ -3963,6 +3963,12 @@ export default class MetamaskController extends EventEmitter {
|
||||
this.approvalController.setFlowLoadingText.bind(
|
||||
this.approvalController,
|
||||
),
|
||||
showApprovalSuccess: this.approvalController.success.bind(
|
||||
this.approvalController,
|
||||
),
|
||||
showApprovalError: this.approvalController.error.bind(
|
||||
this.approvalController,
|
||||
),
|
||||
sendMetrics: this.metaMetricsController.trackEvent.bind(
|
||||
this.metaMetricsController,
|
||||
),
|
||||
|
@ -775,6 +775,9 @@
|
||||
}
|
||||
},
|
||||
"@metamask/approval-controller": {
|
||||
"globals": {
|
||||
"console.info": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/approval-controller>nanoid": true,
|
||||
"@metamask/base-controller": true,
|
||||
|
@ -775,6 +775,9 @@
|
||||
}
|
||||
},
|
||||
"@metamask/approval-controller": {
|
||||
"globals": {
|
||||
"console.info": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/approval-controller>nanoid": true,
|
||||
"@metamask/base-controller": true,
|
||||
|
@ -775,6 +775,9 @@
|
||||
}
|
||||
},
|
||||
"@metamask/approval-controller": {
|
||||
"globals": {
|
||||
"console.info": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/approval-controller>nanoid": true,
|
||||
"@metamask/base-controller": true,
|
||||
|
@ -775,6 +775,9 @@
|
||||
}
|
||||
},
|
||||
"@metamask/approval-controller": {
|
||||
"globals": {
|
||||
"console.info": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/approval-controller>nanoid": true,
|
||||
"@metamask/base-controller": true,
|
||||
|
@ -996,6 +996,9 @@
|
||||
}
|
||||
},
|
||||
"@metamask/approval-controller": {
|
||||
"globals": {
|
||||
"console.info": true
|
||||
},
|
||||
"packages": {
|
||||
"@metamask/approval-controller>nanoid": true,
|
||||
"@metamask/base-controller": true,
|
||||
|
@ -100,7 +100,7 @@
|
||||
"resolutions": {
|
||||
"@babel/core": "patch:@babel/core@npm%3A7.21.5#./.yarn/patches/@babel-core-npm-7.21.5-c72c337956.patch",
|
||||
"@babel/runtime": "patch:@babel/runtime@npm%3A7.18.9#./.yarn/patches/@babel-runtime-npm-7.18.9-28ca6b5f61.patch",
|
||||
"@metamask/approval-controller": "^3.3.0",
|
||||
"@metamask/approval-controller": "^3.4.0",
|
||||
"@types/react": "^16.9.53",
|
||||
"analytics-node/axios": "^0.21.2",
|
||||
"ganache-core/lodash": "^4.17.21",
|
||||
@ -226,7 +226,7 @@
|
||||
"@metamask-institutional/transaction-update": "^0.1.21",
|
||||
"@metamask/address-book-controller": "^3.0.0",
|
||||
"@metamask/announcement-controller": "^4.0.0",
|
||||
"@metamask/approval-controller": "^3.3.0",
|
||||
"@metamask/approval-controller": "^3.4.0",
|
||||
"@metamask/assets-controllers": "^9.2.0",
|
||||
"@metamask/base-controller": "^3.0.0",
|
||||
"@metamask/browser-passworder": "^4.1.0",
|
||||
|
@ -2,7 +2,7 @@ const { strict: assert } = require('assert');
|
||||
const FixtureBuilder = require('../fixture-builder');
|
||||
const { convertToHexValue, withFixtures, openDapp } = require('../helpers');
|
||||
|
||||
describe('Swtich ethereum chain', function () {
|
||||
describe('Switch ethereum chain', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
|
@ -12,6 +12,8 @@ import TextField from '../../ui/text-field';
|
||||
import ConfirmationNetworkSwitch from '../../../pages/confirmation/components/confirmation-network-switch';
|
||||
import UrlIcon from '../../ui/url-icon';
|
||||
import Tooltip from '../../ui/tooltip/tooltip';
|
||||
import { AvatarIcon } from '../../component-library';
|
||||
import ActionableMessage from '../../ui/actionable-message/actionable-message';
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
import { SnapDelineator } from '../snaps/snap-delineator';
|
||||
import { Copyable } from '../snaps/copyable';
|
||||
@ -21,19 +23,21 @@ import { SnapUIMarkdown } from '../snaps/snap-ui-markdown';
|
||||
|
||||
export const safeComponentList = {
|
||||
a: 'a',
|
||||
ActionableMessage,
|
||||
AvatarIcon,
|
||||
b: 'b',
|
||||
i: 'i',
|
||||
p: 'p',
|
||||
div: 'div',
|
||||
span: 'span',
|
||||
Box,
|
||||
Button,
|
||||
Chip,
|
||||
ConfirmationNetworkSwitch,
|
||||
DefinitionList,
|
||||
div: 'div',
|
||||
i: 'i',
|
||||
MetaMaskTranslation,
|
||||
NetworkDisplay,
|
||||
p: 'p',
|
||||
Popover,
|
||||
span: 'span',
|
||||
TextArea,
|
||||
TextField,
|
||||
Tooltip,
|
||||
@ -41,9 +45,9 @@ export const safeComponentList = {
|
||||
Typography,
|
||||
UrlIcon,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
SnapDelineator,
|
||||
Copyable,
|
||||
Spinner,
|
||||
SnapDelineator,
|
||||
SnapUIMarkdown,
|
||||
Spinner,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
};
|
||||
|
@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`error template matches the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="confirmation-page"
|
||||
>
|
||||
<div
|
||||
class="confirmation-page__content"
|
||||
>
|
||||
<div
|
||||
class="box box--padding-4 box--flex-direction-column box--align-items-center box--height-full box--display-flex"
|
||||
>
|
||||
<h2
|
||||
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h2 typography--weight-normal typography--style-normal typography--color-text-default"
|
||||
>
|
||||
Error mock
|
||||
</h2>
|
||||
<div
|
||||
class="box box--padding-top-2 box--padding-bottom-2 box--flex-direction-column box--justify-content-center box--align-items-center box--height-full box--display-flex"
|
||||
>
|
||||
<div
|
||||
class="box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-icon mm-text--body-lg-medium mm-text--text-transform-uppercase box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-error-default box--background-color-error-muted box--rounded-full box--border-color-transparent box--border-style-solid box--border-width-1"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-xl box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/warning.svg');"
|
||||
/>
|
||||
</div>
|
||||
<h3
|
||||
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h3 typography--weight-bold typography--style-normal typography--color-text-default"
|
||||
>
|
||||
Error
|
||||
</h3>
|
||||
<div
|
||||
class="box box--flex-direction-row box--align-items-center box--text-align-center box--display-flex"
|
||||
>
|
||||
<div
|
||||
class="actionable-message actionable-message--danger"
|
||||
>
|
||||
|
||||
<div
|
||||
class="actionable-message__message"
|
||||
>
|
||||
The operation failed.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="confirmation-footer"
|
||||
>
|
||||
<div
|
||||
class="confirmation-footer__actions"
|
||||
>
|
||||
<button
|
||||
class="button btn--rounded btn-primary centered"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`success template matches the snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="confirmation-page"
|
||||
>
|
||||
<div
|
||||
class="confirmation-page__content"
|
||||
>
|
||||
<div
|
||||
class="box box--padding-4 box--flex-direction-column box--align-items-center box--height-full box--display-flex"
|
||||
>
|
||||
<h2
|
||||
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h2 typography--weight-normal typography--style-normal typography--color-text-default"
|
||||
>
|
||||
Success mock
|
||||
</h2>
|
||||
<div
|
||||
class="box box--padding-top-2 box--padding-bottom-2 box--flex-direction-column box--justify-content-center box--align-items-center box--height-full box--display-flex"
|
||||
>
|
||||
<div
|
||||
class="box mm-text mm-avatar-base mm-avatar-base--size-xl mm-avatar-icon mm-text--body-lg-medium mm-text--text-transform-uppercase box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-success-default box--background-color-success-muted box--rounded-full box--border-color-transparent box--border-style-solid box--border-width-1"
|
||||
>
|
||||
<span
|
||||
class="box mm-icon mm-icon--size-xl box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||
style="mask-image: url('./images/icons/confirmation.svg');"
|
||||
/>
|
||||
</div>
|
||||
<h3
|
||||
class="box box--margin-top-1 box--margin-bottom-1 box--flex-direction-row typography typography--h3 typography--weight-bold typography--style-normal typography--color-text-default"
|
||||
>
|
||||
Success
|
||||
</h3>
|
||||
<div
|
||||
class="box box--flex-direction-row box--align-items-center box--text-align-center box--display-flex"
|
||||
>
|
||||
Success message
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="confirmation-footer"
|
||||
>
|
||||
<div
|
||||
class="confirmation-footer__actions"
|
||||
>
|
||||
<button
|
||||
class="button btn--rounded btn-primary centered"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Ok
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
94
ui/pages/confirmation/templates/error.js
Normal file
94
ui/pages/confirmation/templates/error.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { IconName, IconSize } from '../../../components/component-library';
|
||||
import {
|
||||
FontWeight,
|
||||
TextAlign,
|
||||
BlockSize,
|
||||
AlignItems,
|
||||
FlexDirection,
|
||||
JustifyContent,
|
||||
TypographyVariant,
|
||||
IconColor,
|
||||
BackgroundColor,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { processError } from '../util';
|
||||
|
||||
function getValues(pendingApproval, t, actions, _history) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
key: 'header',
|
||||
element: 'Box',
|
||||
props: {
|
||||
flexDirection: FlexDirection.Column,
|
||||
alignItems: AlignItems.center,
|
||||
height: BlockSize.Full,
|
||||
padding: 4,
|
||||
},
|
||||
children: [
|
||||
...(pendingApproval.requestData.header || []),
|
||||
{
|
||||
key: 'content',
|
||||
element: 'Box',
|
||||
props: {
|
||||
flexDirection: FlexDirection.Column,
|
||||
alignItems: AlignItems.center,
|
||||
justifyContent: JustifyContent.center,
|
||||
height: BlockSize.Full,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: 'icon',
|
||||
element: 'AvatarIcon',
|
||||
props: {
|
||||
iconName: IconName.Warning,
|
||||
size: IconSize.Xl,
|
||||
iconProps: { size: IconSize.Xl },
|
||||
color: IconColor.errorDefault,
|
||||
backgroundColor: BackgroundColor.errorMuted,
|
||||
},
|
||||
children: 'Icon',
|
||||
},
|
||||
{
|
||||
key: 'heading',
|
||||
element: 'Typography',
|
||||
props: {
|
||||
variant: TypographyVariant.H3,
|
||||
fontWeight: FontWeight.Bold,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
children: t('resultPageError'),
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
element: 'Box',
|
||||
props: {
|
||||
alignItems: AlignItems.center,
|
||||
textAlign: TextAlign.Center,
|
||||
},
|
||||
children: processError(
|
||||
pendingApproval.requestData.error,
|
||||
t('resultPageErrorDefaultMessage'),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
submitText: t('ok'),
|
||||
onSubmit: () =>
|
||||
actions.resolvePendingApproval(
|
||||
pendingApproval.id,
|
||||
pendingApproval.requestData,
|
||||
),
|
||||
networkDisplay: false,
|
||||
};
|
||||
}
|
||||
|
||||
const error = {
|
||||
getValues,
|
||||
};
|
||||
|
||||
export default error;
|
66
ui/pages/confirmation/templates/error.test.js
Normal file
66
ui/pages/confirmation/templates/error.test.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { ApprovalType } from '@metamask/controller-utils';
|
||||
import { renderWithProvider } from '../../../../test/lib/render-helpers';
|
||||
import Confirmation from '../confirmation';
|
||||
|
||||
jest.mock('../../../../shared/lib/fetch-with-cache');
|
||||
|
||||
const middleware = [thunk];
|
||||
const mockApprovalId = 1;
|
||||
const mockApproval = {
|
||||
id: mockApprovalId,
|
||||
origin: 'https://test-dapp.metamask.io',
|
||||
requestData: {
|
||||
header: [
|
||||
{
|
||||
key: 'headerText',
|
||||
element: 'Typography',
|
||||
children: 'Error mock',
|
||||
props: {
|
||||
variant: 'h2',
|
||||
class: 'header-mock-class',
|
||||
},
|
||||
},
|
||||
],
|
||||
message: 'Error message',
|
||||
},
|
||||
};
|
||||
|
||||
const mockBaseStore = {
|
||||
metamask: {
|
||||
pendingApprovals: {
|
||||
[mockApprovalId]: mockApproval,
|
||||
},
|
||||
approvalFlows: [],
|
||||
subjectMetadata: {},
|
||||
},
|
||||
};
|
||||
|
||||
describe('error template', () => {
|
||||
it('matches the snapshot', async () => {
|
||||
const testStore = {
|
||||
metamask: {
|
||||
...mockBaseStore.metamask,
|
||||
pendingApprovals: {
|
||||
[mockApprovalId]: {
|
||||
...mockApproval,
|
||||
type: ApprovalType.ResultError,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = configureMockStore(middleware)(testStore);
|
||||
const { getByText, container } = renderWithProvider(
|
||||
<Confirmation />,
|
||||
store,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Error mock')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
@ -8,6 +8,8 @@ import {
|
||||
} from '../../../store/actions';
|
||||
import addEthereumChain from './add-ethereum-chain';
|
||||
import switchEthereumChain from './switch-ethereum-chain';
|
||||
import success from './success';
|
||||
import error from './error';
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
import snapAlert from './snaps/snap-alert/snap-alert';
|
||||
import snapConfirmation from './snaps/snap-confirmation/snap-confirmation';
|
||||
@ -17,6 +19,9 @@ import snapPrompt from './snaps/snap-prompt/snap-prompt';
|
||||
const APPROVAL_TEMPLATES = {
|
||||
[ApprovalType.AddEthereumChain]: addEthereumChain,
|
||||
[ApprovalType.SwitchEthereumChain]: switchEthereumChain,
|
||||
// Use ApprovalType from utils controller
|
||||
[ApprovalType.ResultSuccess]: success,
|
||||
[ApprovalType.ResultError]: error,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(snaps)
|
||||
[ApprovalType.SnapDialogAlert]: snapAlert,
|
||||
[ApprovalType.SnapDialogConfirmation]: snapConfirmation,
|
||||
|
94
ui/pages/confirmation/templates/success.js
Normal file
94
ui/pages/confirmation/templates/success.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { IconName, IconSize } from '../../../components/component-library';
|
||||
import {
|
||||
FontWeight,
|
||||
BlockSize,
|
||||
AlignItems,
|
||||
FlexDirection,
|
||||
JustifyContent,
|
||||
TypographyVariant,
|
||||
TextAlign,
|
||||
IconColor,
|
||||
BackgroundColor,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import { processString } from '../util';
|
||||
|
||||
function getValues(pendingApproval, t, actions, _history) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
key: 'header',
|
||||
element: 'Box',
|
||||
props: {
|
||||
flexDirection: FlexDirection.Column,
|
||||
alignItems: AlignItems.center,
|
||||
height: BlockSize.Full,
|
||||
padding: 4,
|
||||
},
|
||||
children: [
|
||||
...(pendingApproval.requestData.header || []),
|
||||
{
|
||||
key: 'content',
|
||||
element: 'Box',
|
||||
props: {
|
||||
flexDirection: FlexDirection.Column,
|
||||
alignItems: AlignItems.center,
|
||||
justifyContent: JustifyContent.center,
|
||||
height: BlockSize.Full,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
key: 'icon',
|
||||
element: 'AvatarIcon',
|
||||
props: {
|
||||
iconName: IconName.Confirmation,
|
||||
size: IconSize.Xl,
|
||||
iconProps: { size: IconSize.Xl },
|
||||
color: IconColor.successDefault,
|
||||
backgroundColor: BackgroundColor.successMuted,
|
||||
},
|
||||
children: 'Icon',
|
||||
},
|
||||
{
|
||||
key: 'heading',
|
||||
element: 'Typography',
|
||||
props: {
|
||||
variant: TypographyVariant.H3,
|
||||
fontWeight: FontWeight.Bold,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
children: t('resultPageSuccess'),
|
||||
},
|
||||
{
|
||||
key: 'message',
|
||||
element: 'Box',
|
||||
props: {
|
||||
alignItems: AlignItems.center,
|
||||
textAlign: TextAlign.Center,
|
||||
},
|
||||
children: processString(
|
||||
pendingApproval.requestData.message,
|
||||
t('resultPageSuccessDefaultMessage'),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
submitText: t('ok'),
|
||||
onSubmit: () =>
|
||||
actions.resolvePendingApproval(
|
||||
pendingApproval.id,
|
||||
pendingApproval.requestData,
|
||||
),
|
||||
networkDisplay: false,
|
||||
};
|
||||
}
|
||||
|
||||
const success = {
|
||||
getValues,
|
||||
};
|
||||
|
||||
export default success;
|
66
ui/pages/confirmation/templates/success.test.js
Normal file
66
ui/pages/confirmation/templates/success.test.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
import { ApprovalType } from '@metamask/controller-utils';
|
||||
import { renderWithProvider } from '../../../../test/lib/render-helpers';
|
||||
import Confirmation from '../confirmation';
|
||||
|
||||
jest.mock('../../../../shared/lib/fetch-with-cache');
|
||||
|
||||
const middleware = [thunk];
|
||||
const mockApprovalId = 1;
|
||||
const mockApproval = {
|
||||
id: mockApprovalId,
|
||||
origin: 'https://test-dapp.metamask.io',
|
||||
requestData: {
|
||||
header: [
|
||||
{
|
||||
key: 'headerText',
|
||||
element: 'Typography',
|
||||
children: 'Success mock',
|
||||
props: {
|
||||
variant: 'h2',
|
||||
class: 'header-mock-class',
|
||||
},
|
||||
},
|
||||
],
|
||||
message: 'Success message',
|
||||
},
|
||||
};
|
||||
|
||||
const mockBaseStore = {
|
||||
metamask: {
|
||||
pendingApprovals: {
|
||||
[mockApprovalId]: mockApproval,
|
||||
},
|
||||
approvalFlows: [],
|
||||
subjectMetadata: {},
|
||||
},
|
||||
};
|
||||
|
||||
describe('success template', () => {
|
||||
it('matches the snapshot', async () => {
|
||||
const testStore = {
|
||||
metamask: {
|
||||
...mockBaseStore.metamask,
|
||||
pendingApprovals: {
|
||||
[mockApprovalId]: {
|
||||
...mockApproval,
|
||||
type: ApprovalType.ResultSuccess,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = configureMockStore(middleware)(testStore);
|
||||
const { getByText, container } = renderWithProvider(
|
||||
<Confirmation />,
|
||||
store,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText('Success mock')).toBeInTheDocument();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
60
ui/pages/confirmation/util.test.ts
Normal file
60
ui/pages/confirmation/util.test.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { ResultComponent } from '@metamask/approval-controller';
|
||||
import { processError, processString } from './util';
|
||||
|
||||
const FALLBACK_MESSAGE = 'Fallback Message';
|
||||
const mockResultComponent: ResultComponent = {
|
||||
key: 'mock-key',
|
||||
name: 'mock-component',
|
||||
properties: { message: 'mock1', message2: 'mock2' },
|
||||
children: 'Mock child',
|
||||
};
|
||||
|
||||
const expectedTemplateRendererComponent = {
|
||||
key: 'mock-key',
|
||||
props: {
|
||||
message: 'mock1',
|
||||
message2: 'mock2',
|
||||
},
|
||||
children: 'Mock child',
|
||||
element: 'mock-component',
|
||||
};
|
||||
|
||||
describe('processError', () => {
|
||||
it('returns TemplateRendererComponent when input is not defined', () => {
|
||||
const result = processError(undefined, FALLBACK_MESSAGE);
|
||||
expect(result).toEqual({
|
||||
key: 'error',
|
||||
element: 'ActionableMessage',
|
||||
props: { type: 'danger', message: FALLBACK_MESSAGE },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns TemplateRendererComponent when input is a string', () => {
|
||||
const result = processError('Error Message', FALLBACK_MESSAGE);
|
||||
expect(result).toEqual({
|
||||
key: 'error',
|
||||
element: 'ActionableMessage',
|
||||
props: { type: 'danger', message: 'Error Message' },
|
||||
});
|
||||
});
|
||||
it('returns TemplateRendererComponent when input is a ResultComponent', () => {
|
||||
const result = processError(mockResultComponent, FALLBACK_MESSAGE);
|
||||
expect(result).toEqual(expectedTemplateRendererComponent);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processString', () => {
|
||||
it('returns string when input is not defined', () => {
|
||||
const result = processString(undefined, FALLBACK_MESSAGE);
|
||||
expect(result[0]).toEqual(FALLBACK_MESSAGE);
|
||||
});
|
||||
|
||||
it('returns TemplateRendererComponent when input is a string', () => {
|
||||
const result = processString('Hello', FALLBACK_MESSAGE);
|
||||
expect(result).toEqual(['Hello']);
|
||||
});
|
||||
it('returns TemplateRendererComponent when input is a ResultComponent', () => {
|
||||
const result = processString(mockResultComponent, FALLBACK_MESSAGE);
|
||||
expect(result).toEqual(expectedTemplateRendererComponent);
|
||||
});
|
||||
});
|
150
ui/pages/confirmation/util.ts
Normal file
150
ui/pages/confirmation/util.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { ResultComponent } from '@metamask/approval-controller';
|
||||
|
||||
type TemplateRendererComponent = {
|
||||
key: string;
|
||||
element: string;
|
||||
props?: Record<string, unknown>;
|
||||
children?:
|
||||
| string
|
||||
| TemplateRendererComponent
|
||||
| (string | TemplateRendererComponent)[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes an error message or ResultComponent and returns a TemplateRendererComponent
|
||||
* or an array of strings | TemplateRendererComponents.
|
||||
*
|
||||
* @param input - The message or component to process.
|
||||
* @param fallback - The fallback message to use when the input is not valid.
|
||||
* @returns The processed error component.
|
||||
*/
|
||||
export function processError(
|
||||
input: undefined | string | ResultComponent | ResultComponent[],
|
||||
fallback: string,
|
||||
): TemplateRendererComponent | (string | TemplateRendererComponent)[] {
|
||||
const currentInput = convertResultComponents(input) || fallback;
|
||||
|
||||
if (typeof currentInput !== 'string') {
|
||||
return currentInput;
|
||||
}
|
||||
|
||||
return {
|
||||
key: 'error',
|
||||
element: 'ActionableMessage',
|
||||
props: { type: 'danger', message: currentInput },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a string or ResultComponent and returns a string or TemplateRendererComponent
|
||||
* or an array of strings | TemplateRendererComponents.
|
||||
*
|
||||
* @param input - The message or component to process.
|
||||
* @param fallback - The fallback string to use when the input is not valid.
|
||||
* @returns The processed message.
|
||||
*/
|
||||
export function processString(
|
||||
input: undefined | string | ResultComponent | ResultComponent[],
|
||||
fallback: string,
|
||||
): string | TemplateRendererComponent | (string | TemplateRendererComponent)[] {
|
||||
const currentInput = convertResultComponents(input) || fallback;
|
||||
|
||||
if (typeof currentInput !== 'string') {
|
||||
return currentInput;
|
||||
}
|
||||
|
||||
return applyBold(currentInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies bold formatting to the message.
|
||||
*
|
||||
* @param message - The input message to apply bold formatting to.
|
||||
* @returns The formatted message.
|
||||
*/
|
||||
function applyBold(message: string): (string | TemplateRendererComponent)[] {
|
||||
const boldPattern = /\*\*(.+?)\*\*/gu;
|
||||
|
||||
return findMarkdown(message, boldPattern, (formattedText, index) => ({
|
||||
key: `bold-${index}`,
|
||||
element: 'b',
|
||||
children: formattedText,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and formats markdown elements in the given text.
|
||||
*
|
||||
* @param text - The input text to search for markdown elements.
|
||||
* @param pattern - The pattern to match the markdown elements.
|
||||
* @param getElement - The callback function to create the formatted elements.
|
||||
* @returns The array of formatted elements.
|
||||
*/
|
||||
function findMarkdown(
|
||||
text: string,
|
||||
pattern: RegExp,
|
||||
getElement: (
|
||||
formattedText: string,
|
||||
index: number,
|
||||
) => TemplateRendererComponent,
|
||||
): (string | TemplateRendererComponent)[] {
|
||||
let position = 0;
|
||||
let index = 0;
|
||||
|
||||
const matches = Array.from(text.matchAll(pattern));
|
||||
const elements = [];
|
||||
|
||||
for (const match of matches) {
|
||||
const rawText = text.substring(position, match.index);
|
||||
|
||||
if (rawText.length) {
|
||||
elements.push(rawText);
|
||||
}
|
||||
|
||||
const formattedText = match[1];
|
||||
const formattedElement = getElement(formattedText, index);
|
||||
|
||||
elements.push(formattedElement);
|
||||
|
||||
position = (match.index as number) + match[0].length;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
const finalRawText = text.substring(position);
|
||||
|
||||
if (finalRawText.length) {
|
||||
elements.push(finalRawText);
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
function convertResultComponents(
|
||||
input: undefined | string | ResultComponent | (string | ResultComponent)[],
|
||||
):
|
||||
| undefined
|
||||
| string
|
||||
| TemplateRendererComponent
|
||||
| (string | TemplateRendererComponent)[] {
|
||||
if (input === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof input === 'string') {
|
||||
return input;
|
||||
}
|
||||
|
||||
if (Array.isArray(input)) {
|
||||
return input.map(convertResultComponents) as (
|
||||
| string
|
||||
| TemplateRendererComponent
|
||||
)[];
|
||||
}
|
||||
|
||||
return {
|
||||
key: input.key,
|
||||
element: input.name,
|
||||
props: input.properties,
|
||||
children: convertResultComponents(input.children),
|
||||
};
|
||||
}
|
10
yarn.lock
10
yarn.lock
@ -3857,16 +3857,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@metamask/approval-controller@npm:^3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "@metamask/approval-controller@npm:3.3.0"
|
||||
"@metamask/approval-controller@npm:^3.4.0":
|
||||
version: 3.4.0
|
||||
resolution: "@metamask/approval-controller@npm:3.4.0"
|
||||
dependencies:
|
||||
"@metamask/base-controller": ^3.0.0
|
||||
"@metamask/utils": ^5.0.2
|
||||
eth-rpc-errors: ^4.0.2
|
||||
immer: ^9.0.6
|
||||
nanoid: ^3.1.31
|
||||
checksum: 1fa6111a897d6f4aa369fd1a669fb5e16558277019f9d6c61449aea0d7b2672a62c189e5b3d9e84ab6e4d5826932c78d2bdb0f05aafb184a4ff07903c46abf2c
|
||||
checksum: 153136800fbd8cd50e2d6012740526c55e74e3f8e5aa99a05d5788e8bae04ede622608a1adf67dcf0986162e9e7d1d11b6f6f0df438904b65db7435b881c2e3f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -24596,7 +24596,7 @@ __metadata:
|
||||
"@metamask-institutional/transaction-update": ^0.1.21
|
||||
"@metamask/address-book-controller": ^3.0.0
|
||||
"@metamask/announcement-controller": ^4.0.0
|
||||
"@metamask/approval-controller": ^3.3.0
|
||||
"@metamask/approval-controller": ^3.4.0
|
||||
"@metamask/assets-controllers": ^9.2.0
|
||||
"@metamask/auto-changelog": ^2.1.0
|
||||
"@metamask/base-controller": ^3.0.0
|
||||
|
Loading…
Reference in New Issue
Block a user