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

Provide New UI Components for EIP-1559 Designs (#11357)

This commit is contained in:
David Walsh 2021-06-23 18:39:44 -05:00 committed by GitHub
parent 85de65f470
commit 6fa36cdf51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 860 additions and 3 deletions

View File

@ -798,6 +798,9 @@
"gasFee": {
"message": "Gas Fee"
},
"gasFeeEstimate": {
"message": "Estimate"
},
"gasLimit": {
"message": "Gas Limit"
},
@ -1085,6 +1088,12 @@
"max": {
"message": "Max"
},
"maxFee": {
"message": "Max fee"
},
"maxPriorityFee": {
"message": "Max priority fee"
},
"memo": {
"message": "memo"
},
@ -1478,6 +1487,9 @@
"recipientAddressPlaceholder": {
"message": "Search, public address (0x), or ENS"
},
"recommendedGasLabel": {
"message": "Recommended"
},
"recoveryPhraseReminderBackupStart": {
"message": "Start here"
},

View File

@ -433,6 +433,7 @@ function getEnvironmentVariables({ devMode, testing }) {
PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
CONF: devMode ? metamaskrc : {},
SHOW_EIP_1559_UI: process.env.SHOW_EIP_1559_UI === '1',
SENTRY_DSN: process.env.SENTRY_DSN,
SENTRY_DSN_DEV: metamaskrc.SENTRY_DSN_DEV,
INFURA_PROJECT_ID: testing

View File

@ -0,0 +1,87 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Typography from '../../ui/typography/typography';
import {
COLORS,
TEXT_ALIGN,
DISPLAY,
TYPOGRAPHY,
FONT_WEIGHT,
} from '../../../helpers/constants/design-system';
import NumericInput from '../../ui/numeric-input/numeric-input.component';
import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
export default function AdvancedGasControlsRow({
titleText,
tooltipText,
titleDetailText,
error,
onChange,
value,
}) {
return (
<div
className={classNames('advanced-gas-controls__row', {
'advanced-gas-controls__row--error': error,
})}
>
<label>
<div className="advanced-gas-controls__row-heading">
<div className="advanced-gas-controls__row-heading-title">
<Typography
tag={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H6}
boxProps={{ display: DISPLAY.INLINE_BLOCK }}
>
{titleText}
</Typography>
<InfoTooltip position="top" contentText={tooltipText} />
</div>
{titleDetailText && (
<Typography
className="advanced-gas-controls__row-heading-detail"
align={TEXT_ALIGN.END}
color={COLORS.UI4}
variant={TYPOGRAPHY.H8}
>
{titleDetailText}
</Typography>
)}
</div>
<NumericInput error={error} onChange={onChange} value={value} />
{error && (
<Typography
color={COLORS.ERROR1}
variant={TYPOGRAPHY.H7}
className="advanced-gas-controls__row-error"
>
{error}
</Typography>
)}
</label>
</div>
);
}
AdvancedGasControlsRow.propTypes = {
titleText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
tooltipText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
titleDetailText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
error: PropTypes.string,
onChange: PropTypes.func,
value: PropTypes.number,
};
AdvancedGasControlsRow.defaultProps = {
titleText: '',
tooltipText: '',
titleDetailText: '',
error: '',
onChange: undefined,
value: 0,
};

View File

@ -0,0 +1,75 @@
import React, { useContext, useState } from 'react';
import { I18nContext } from '../../../contexts/i18n';
import Typography from '../../ui/typography/typography';
import {
FONT_WEIGHT,
TYPOGRAPHY,
COLORS,
} from '../../../helpers/constants/design-system';
import AdvancedGasControlsRow from './advanced-gas-controls-row.component';
export default function AdvancedGasControls() {
const t = useContext(I18nContext);
const [gasLimit, setGasLimit] = useState(0);
const [maxPriorityFee, setMaxPriorityFee] = useState(0);
const [maxFee, setMaxFee] = useState(0);
return (
<div className="advanced-gas-controls">
<AdvancedGasControlsRow
titleText={t('gasLimit')}
onChange={setGasLimit}
tooltipText=""
titleDetailText=""
value={gasLimit}
/>
<AdvancedGasControlsRow
titleText={t('maxPriorityFee')}
tooltipText=""
onChange={setMaxPriorityFee}
value={maxPriorityFee}
titleDetailText={
<>
<Typography
tag="span"
color={COLORS.UI4}
variant={TYPOGRAPHY.H8}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('gasFeeEstimate')}:
</Typography>{' '}
<Typography
tag="span"
color={COLORS.UI4}
variant={TYPOGRAPHY.H8}
></Typography>
</>
}
/>
<AdvancedGasControlsRow
titleText={t('maxFee')}
tooltipText=""
onChange={setMaxFee}
value={maxFee}
titleDetailText={
<>
<Typography
tag="span"
color={COLORS.UI4}
variant={TYPOGRAPHY.H8}
fontWeight={FONT_WEIGHT.BOLD}
>
{t('gasFeeEstimate')}:
</Typography>{' '}
<Typography
tag="span"
color={COLORS.UI4}
variant={TYPOGRAPHY.H8}
></Typography>
</>
}
/>
</div>
);
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import AdvancedGasControls from '.';
export default {
title: 'Advanced Gas Controls',
};
export const simple = () => {
return (
<div style={{ width: '600px' }}>
<AdvancedGasControls />
</div>
);
};

View File

@ -0,0 +1 @@
export { default } from './advanced-gas-controls.component';

View File

@ -0,0 +1,34 @@
.advanced-gas-controls {
&__row {
margin-bottom: 20px;
}
&__row-heading {
display: flex;
}
.info-tooltip {
display: inline-block;
}
&__row-heading-detail {
flex-grow: 1;
align-self: center;
}
&__row-error,
&__row--error h6 {
color: $error-1 !important;
padding-top: 6px;
}
h6 {
padding-bottom: 6px;
margin-inline-end: 6px;
}
i {
color: #dadada;
font-size: $font-size-h7;
}
}

View File

@ -2,6 +2,7 @@
@import 'account-list-item/index';
@import 'account-menu/index';
@import 'add-token-button/index';
@import 'advanced-gas-controls/index';
@import 'alerts/alerts';
@import 'app-header/index';
@import 'asset-list-item/asset-list-item';
@ -10,6 +11,7 @@
@import 'connected-accounts-permissions/index';
@import 'connected-sites-list/index';
@import 'connected-status-indicator/index';
@import 'edit-gas-display/index';
@import 'gas-customization/gas-modal-page-container/index';
@import 'gas-customization/gas-price-button-group/index';
@import 'gas-customization/index';
@ -32,10 +34,13 @@
@import 'token-cell/token-cell';
@import 'transaction-activity-log/index';
@import 'transaction-breakdown/index';
@import 'transaction-detail/index';
@import 'transaction-detail-item/index';
@import 'transaction-icon/transaction-icon';
@import 'transaction-list-item-details/index';
@import 'transaction-list-item/index';
@import 'transaction-list/index';
@import 'transaction-status/index';
@import 'transaction-total-banner/index';
@import 'wallet-overview/index';
@import 'whats-new-popup/index';

View File

@ -0,0 +1,45 @@
import React, { useState, useContext } from 'react';
import TransactionTotalBanner from '../transaction-total-banner/transaction-total-banner.component';
import RadioGroup from '../../ui/radio-group/radio-group.component';
import AdvancedGasControls from '../advanced-gas-controls/advanced-gas-controls.component';
import { I18nContext } from '../../../contexts/i18n';
export default function EditGasDisplay() {
const t = useContext(I18nContext);
const [showAdvancedForm, setShowAdvancedForm] = useState(false);
return (
<div className="edit-gas-display">
<TransactionTotalBanner
total="9.99"
detail="Up to $17.79 (0.01234 ETH)"
timing="Likely in < 30 seconds
"
/>
<RadioGroup
name="gas-recommendation"
options={[
{ value: 'low', label: 'Low', recommended: false },
{ value: 'medium', label: 'Medium', recommended: false },
{ value: 'high', label: 'High', recommended: true },
]}
selectedValue="high"
/>
<button
className="edit-gas-display__advanced-button"
onClick={() => setShowAdvancedForm(!showAdvancedForm)}
>
{t('advancedOptions')}{' '}
{showAdvancedForm ? (
<i className="fa fa-caret-up"></i>
) : (
<i className="fa fa-caret-down"></i>
)}
</button>
{showAdvancedForm && <AdvancedGasControls />}
</div>
);
}

View File

@ -0,0 +1,36 @@
import React from 'react';
import PopoverPortal from '../../ui/popover/popover.component';
import Button from '../../ui/button';
import EditGasDisplay from '.';
export default {
title: 'Edit Gas Display',
};
export const basic = () => {
return (
<div style={{ width: '600px' }}>
<EditGasDisplay />
</div>
);
};
export const insidePopover = () => {
return (
<div style={{ width: '600px' }}>
<PopoverPortal
title="Edit gas fee"
onClose={() => console.log('Closing!')}
footer={
<>
<Button type="primary">Save</Button>
</>
}
>
<div style={{ padding: '20px' }}>
<EditGasDisplay />
</div>
</PopoverPortal>
</div>
);
};

View File

@ -0,0 +1 @@
export { default } from './edit-gas-display';

View File

@ -0,0 +1,17 @@
.edit-gas-display {
.radio-group {
margin: 20px auto;
}
&__advanced-button {
display: block;
margin: 0 auto;
background: transparent;
color: $primary-1;
font-weight: bold;
}
.advanced-gas-controls {
margin-top: 20px;
}
}

View File

@ -0,0 +1 @@
export { default } from './transaction-detail-item.component';

View File

@ -0,0 +1,29 @@
.transaction-detail-item {
color: $ui-4;
&__row {
display: flex;
}
&__title {
flex-grow: 1;
}
.info-tooltip {
display: inline-block;
margin-inline-start: 4px;
}
&__detail-text {
margin-inline-end: 20px !important;
}
&__total {
font-weight: bold;
color: $ui-black;
}
&__subtitle {
flex-grow: 1;
}
}

View File

@ -0,0 +1,71 @@
import React from 'react';
import PropTypes from 'prop-types';
import Typography from '../../ui/typography/typography';
import {
COLORS,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
export default function TransactionDetailItem({
detailTitle,
detailText,
detailTotal,
subTitle,
subText,
}) {
return (
<div className="transaction-detail-item">
<div className="transaction-detail-item__row">
<Typography
color={COLORS.BLACK}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H6}
className="transaction-detail-item__title"
>
{detailTitle}
</Typography>
{detailText && (
<Typography className="transaction-detail-item__detail-text">
{detailText}
</Typography>
)}
<Typography
color={COLORS.BLACK}
fontWeight={FONT_WEIGHT.BOLD}
className="transaction-detail-item__total"
>
{detailTotal}
</Typography>
</div>
<div className="transaction-detail-item__row">
{subTitle && (
<Typography
variant={TYPOGRAPHY.H7}
className="transaction-detail-item__subtitle"
>
{subTitle}
</Typography>
)}
<Typography variant={TYPOGRAPHY.H7}>{subText}</Typography>
</div>
</div>
);
}
TransactionDetailItem.propTypes = {
detailTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
detailText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
detailTotal: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
subTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
subText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
};
TransactionDetailItem.defaultProps = {
detailTitle: '',
detailText: '',
detailTotal: '',
subTitle: '',
subText: '',
};

View File

@ -0,0 +1,32 @@
import React from 'react';
import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
import TransactionDetailItem from '.';
export default {
title: 'Transaction Detail Item',
};
export const basic = () => {
return (
<div style={{ width: '400px' }}>
<TransactionDetailItem
detailTitle={
<>
<strong>Estimated gas fee</strong>
<InfoTooltip contentText="This is the tooltip text" position="top">
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
}
detailText="16565.30"
detailTotal="0.0089 ETH"
subTitle="Very likely in < 15 seconds"
subText={
<>
From <strong>$16565 - $19000</strong>
</>
}
/>
</div>
);
};

View File

@ -0,0 +1 @@
export { default } from './transaction-detail.component';

View File

@ -0,0 +1,11 @@
.transaction-detail {
.transaction-detail-item {
padding: 20px 0;
border-bottom: 1px solid $ui-3;
&:last-child {
padding-bottom: 0;
border-bottom: 0;
}
}
}

View File

@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
export default function TransactionDetail({ rows }) {
return <div className="transaction-detail">{rows}</div>;
}
TransactionDetail.propTypes = {
rows: PropTypes.arrayOf(TransactionDetailItem),
};
TransactionDetail.defaultProps = {
rows: [],
};

View File

@ -0,0 +1,50 @@
import React from 'react';
import InfoTooltip from '../../ui/info-tooltip/info-tooltip';
import TransactionDetailItem from '../transaction-detail-item/transaction-detail-item.component';
import TransactionDetail from '.';
export default {
title: 'Transaction Detail',
};
const rows = [
<TransactionDetailItem
key="line-1"
detailTitle={
<>
Estimated gas fee
<InfoTooltip contentText="This is the tooltip text" position="top">
<i className="fa fa-info-circle" />
</InfoTooltip>
</>
}
detailText="0.00896 ETH"
detailTotal="$15.73"
subTitle="Very likely in < 15 seconds"
subText={
<>
From <strong>$15.73 - $19.81</strong>
</>
}
/>,
<TransactionDetailItem
key="line-2"
detailTitle="Total"
detailText=".0312 ETH"
detailTotal="$15.77"
subTitle="Amount + gas fee"
subText={
<>
Up to <strong>$19.85</strong>
</>
}
/>,
];
export const basic = () => {
return (
<div style={{ width: '400px' }}>
<TransactionDetail rows={rows} />
</div>
);
};

View File

@ -0,0 +1 @@
export { default } from './transaction-total-banner.component';

View File

@ -0,0 +1,7 @@
.transaction-total-banner {
text-align: center;
&__detail {
padding-bottom: 4px;
}
}

View File

@ -0,0 +1,45 @@
import React from 'react';
import PropTypes from 'prop-types';
import Typography from '../../ui/typography/typography';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
export default function TransactionTotalBanner({ total, detail, timing }) {
return (
<div className="transaction-total-banner">
<Typography color={COLORS.BLACK} variant={TYPOGRAPHY.H1}>
{total}
</Typography>
{detail && (
<Typography
color={COLORS.BLACK}
variant={TYPOGRAPHY.H6}
className="transaction-total-banner__detail"
>
{detail}
</Typography>
)}
{timing && (
<Typography
color={COLORS.UI4}
variant={TYPOGRAPHY.H7}
className="transaction-total-banner__timing"
>
{timing}
</Typography>
)}
</div>
);
}
TransactionTotalBanner.propTypes = {
total: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
detail: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
timing: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
};
TransactionTotalBanner.defaultProps = {
total: '',
detail: '',
timing: '',
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import TransactionTotalBanner from '.';
export default {
title: 'Transaction Total Banner',
};
export const basic = () => {
return (
<TransactionTotalBanner
total="~18.73"
detail={
<>
Up to <strong>$19.81</strong> (0.01234 ETH)
</>
}
timing="Very likely in < 15 seconds"
/>
);
};

View File

@ -0,0 +1 @@
export { default } from './numeric-input.component';

View File

@ -0,0 +1,39 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Typography from '../typography/typography';
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
export default function NumericInput({ detailText, value, onChange, error }) {
return (
<div
className={classNames('numeric-input', { 'numeric-input--error': error })}
>
<input
type="number"
value={value}
onChange={(e) => onChange?.(Number(e.target.value))}
min="0"
/>
{detailText && (
<Typography color={COLORS.UI4} variant={TYPOGRAPHY.H7} tag="span">
{detailText}
</Typography>
)}
</div>
);
}
NumericInput.propTypes = {
value: PropTypes.number,
detailText: PropTypes.string,
onChange: PropTypes.func,
error: PropTypes.string,
};
NumericInput.defaultProps = {
value: 0,
detailText: '',
onChange: undefined,
error: '',
};

View File

@ -0,0 +1,28 @@
.numeric-input {
border: 1px solid $ui-3;
position: relative;
border-radius: 6px;
&--error {
border-color: $error-1;
}
input {
width: 100%;
border: 0;
padding: 10px;
border-radius: 6px;
/* ensures the increment/decrement arrows always display */
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
opacity: 1;
}
}
span {
position: absolute;
right: 40px;
top: 7px;
}
}

View File

@ -0,0 +1,36 @@
import React from 'react';
import NumericInput from '.';
export default {
title: 'NumericInput',
};
const onChange = (e) => console.log('changed value: ', e.target.value);
export const numericInput = () => {
return (
<div style={{ width: '600px' }}>
<NumericInput onChange={onChange} />
</div>
);
};
export const numericInputWithDetail = () => {
return (
<div style={{ width: '600px' }}>
<NumericInput detailText="= $0.06" onChange={onChange} />
</div>
);
};
export const numericInputWithError = () => {
return (
<div style={{ width: '600px' }}>
<NumericInput
detailText="= $0.06"
error="This number isn't great"
onChange={onChange}
/>
</div>
);
};

View File

@ -0,0 +1 @@
export { default } from './radio-group.component';

View File

@ -0,0 +1,49 @@
.radio-group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 100px;
width: 300px;
label {
cursor: pointer;
}
&__column-recommended {
height: 20px;
}
&__column-line {
width: 1px;
height: 5px;
background-color: $ui-2;
margin: 0 auto;
}
&__column-horizontal-line {
height: 1px;
background-color: $ui-2;
width: 100%;
}
&__column:first-child &__column-horizontal-line {
width: 50px;
margin-left: 50px;
}
&__column:last-child &__column-horizontal-line {
width: 51px;
}
&__column-radio {
margin-inline-end: 1px;
}
&__column-radio,
&__column-label {
text-align: center;
}
&__column-label {
padding-top: 6px;
}
}

View File

@ -0,0 +1,64 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import Typography from '../typography/typography';
import {
COLORS,
FONT_WEIGHT,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
export default function RadioGroup({ options, name, selectedValue, onChange }) {
const t = useContext(I18nContext);
return (
<div className="radio-group">
{options.map((option) => {
return (
<div className="radio-group__column" key={`${name}-${option.value}`}>
<label>
<Typography
color={COLORS.SUCCESS3}
className="radio-group__column-recommended"
variant={TYPOGRAPHY.H7}
>
{option.recommended ? t('recommendedGasLabel') : ''}
</Typography>
<div className="radio-group__column-radio">
<input
type="radio"
name={name}
defaultChecked={option.value === selectedValue}
value={option.value}
onChange={() => onChange?.(option.value)}
/>
</div>
<div className="radio-group__column-line"></div>
<div className="radio-group__column-horizontal-line"></div>
<Typography
color={COLORS.UI4}
fontWeight={FONT_WEIGHT.BOLD}
variant={TYPOGRAPHY.H7}
className="radio-group__column-label"
>
{option.label}
</Typography>
</label>
</div>
);
})}
</div>
);
}
RadioGroup.propTypes = {
options: PropTypes.array,
selectedValue: PropTypes.string,
name: PropTypes.string,
onChange: PropTypes.func,
};
RadioGroup.defaultProps = {
options: [],
};

View File

@ -0,0 +1,22 @@
import React from 'react';
import RadioGroup from '.';
export default {
title: 'RadioGroup',
};
export const radioGroup = () => {
return (
<div className="radio-group" style={{ minWidth: '600px' }}>
<RadioGroup
name="gas-recommendation"
options={[
{ value: 'low', label: 'Low', recommended: false },
{ value: 'medium', label: 'Medium', recommended: false },
{ value: 'high', label: 'High', recommended: true },
]}
selectedValue="high"
/>
</div>
);
};

View File

@ -25,9 +25,11 @@ export default function Typography({
'typography',
className,
`typography--${variant}`,
`typography--align-${align}`,
`typography--color-${color}`,
`typography--weight-${fontWeight}`,
{
[`typography--align-${align}`]: Boolean(align),
[`typography--color-${color}`]: Boolean(color),
},
);
let Tag = tag ?? variant;

View File

@ -33,10 +33,12 @@
@import 'loading-indicator/loading-indicator';
@import 'loading-screen/index';
@import 'menu/menu';
@import 'numeric-input/numeric-input';
@import 'page-container/index';
@import 'popover/index';
@import 'pulse-loader/index';
@import 'qr-code/index';
@import 'radio-group/index';
@import 'readonly-input/index';
@import 'sender-to-recipient/index';
@import 'snackbar/index';

View File

@ -68,5 +68,5 @@ $sizes-strings:
$border-style: solid, double, none, dashed, dotted;
$directions: top, right, bottom, left;
$display: block, grid, flex, inline-block, inline-grid, inline-flex, list-item;
$text-align: left, right, center, justify;
$text-align: left, right, center, justify, end;
$font-weight: bold, normal, 100, 200, 300, 400, 500, 600, 700, 800, 900;

View File

@ -139,6 +139,7 @@ export const TEXT_ALIGN = {
CENTER: 'center',
RIGHT: 'right',
JUSTIFY: 'justify',
END: 'end',
};
export const FONT_WEIGHT = {