1
0
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:
Vinicius Stevam 2023-07-12 09:29:54 +01:00 committed by GitHub
parent 2fad10e0a3
commit 5736e670f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 715 additions and 14 deletions

View File

@ -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"
},

View File

@ -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,
),

View File

@ -775,6 +775,9 @@
}
},
"@metamask/approval-controller": {
"globals": {
"console.info": true
},
"packages": {
"@metamask/approval-controller>nanoid": true,
"@metamask/base-controller": true,

View File

@ -775,6 +775,9 @@
}
},
"@metamask/approval-controller": {
"globals": {
"console.info": true
},
"packages": {
"@metamask/approval-controller>nanoid": true,
"@metamask/base-controller": true,

View File

@ -775,6 +775,9 @@
}
},
"@metamask/approval-controller": {
"globals": {
"console.info": true
},
"packages": {
"@metamask/approval-controller>nanoid": true,
"@metamask/base-controller": true,

View File

@ -775,6 +775,9 @@
}
},
"@metamask/approval-controller": {
"globals": {
"console.info": true
},
"packages": {
"@metamask/approval-controller>nanoid": true,
"@metamask/base-controller": true,

View File

@ -996,6 +996,9 @@
}
},
"@metamask/approval-controller": {
"globals": {
"console.info": true
},
"packages": {
"@metamask/approval-controller>nanoid": true,
"@metamask/base-controller": true,

View File

@ -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",

View File

@ -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: [
{

View File

@ -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
};

View File

@ -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>
`;

View File

@ -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>
`;

View 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;

View 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();
});
});
});

View File

@ -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,

View 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;

View 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();
});
});
});

View 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);
});
});

View 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),
};
}

View File

@ -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