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

Created a custom spending cap component (#15522)

This commit is contained in:
VSaric 2022-09-28 21:46:29 +02:00 committed by GitHub
parent 947f5299f8
commit 8e736e39d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 356 additions and 5 deletions

View File

@ -1259,6 +1259,9 @@
"ensUnknownError": {
"message": "ENS lookup failed."
},
"enterANumber": {
"message": "Enter a number"
},
"enterMaxSpendLimit": {
"message": "Enter max spend limit"
},
@ -1702,6 +1705,16 @@
"initialTransactionConfirmed": {
"message": "Your initial transaction was confirmed by the network. Click OK to go back."
},
"inputLogicEmptyState": {
"message": "Only enter a number that you're comfortable with the contract spending now or in the future. You can always increase the spending cap later."
},
"inputLogicEqualOrSmallerNumber": {
"message": "This allows the contract to spend $1 from your current balance.",
"description": "$1 is the current token balance in the account and the name of the current token"
},
"inputLogicHigherNumber": {
"message": "This allows the contract to spend all your token balance until it reaches the cap or you revoke the spending cap. If this is not intended, consider setting a lower spending cap."
},
"install": {
"message": "Install"
},
@ -3343,6 +3356,13 @@
"spendLimitTooLarge": {
"message": "Spend limit too large"
},
"spendingCapError": {
"message": "Error: Enter numbers only"
},
"spendingCapErrorDescription": {
"message": "Only enter a number that you're comfortable with $1 accessing now or in the future. You can always increase the token limit later.",
"description": "$1 is origin of the site requesting the token limit"
},
"srpInputNumberOfWords": {
"message": "I have a $1-word phrase",
"description": "This is the text for each option in the dropdown where a user selects how many words their secret recovery phrase has during import. The $1 is the number of words (either 12, 15, 18, 21, or 24)."
@ -4223,6 +4243,9 @@
"useCollectibleDetectionDescription": {
"message": "Displaying NFTs media & data may expose your IP address to centralized servers. Third-party APIs (like OpenSea) are used to detect NFTs in your wallet. This exposes your account address with those services. Leave this disabled if you dont want the app to pull data from those those services."
},
"useDefault": {
"message": "Use default"
},
"usePhishingDetection": {
"message": "Use phishing detection"
},

View File

@ -22,6 +22,7 @@
@import 'connected-sites-list/index';
@import 'connected-status-indicator/index';
@import 'create-new-vault/create-new-vault.scss';
@import 'custom-spending-cap/index';
@import 'edit-gas-display/index';
@import 'edit-gas-display-education/index';
@import 'edit-gas-fee-button/index';

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import Box from '../../ui/box';
import Typography from '../../ui/typography';
import Tooltip from '../../ui/tooltip';
import {
COLORS,
DISPLAY,
TYPOGRAPHY,
} from '../../../helpers/constants/design-system';
export const CustomSpendingCapTooltip = ({
tooltipContentText,
tooltipIcon,
}) => (
<Box display={DISPLAY.INLINE_BLOCK}>
<Tooltip
interactive
position="top"
html={
<Typography
variant={TYPOGRAPHY.H7}
margin={3}
color={COLORS.TEXT_ALTERNATIVE}
className="form-field__heading-title__tooltip"
>
{tooltipContentText}
</Typography>
}
>
{tooltipIcon ? (
<i className="fa fa-exclamation-triangle form-field__heading-title__tooltip__warning-icon" />
) : (
tooltipIcon !== '' && <i className="fa fa-question-circle" />
)}
</Tooltip>
</Box>
);
CustomSpendingCapTooltip.propTypes = {
tooltipContentText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
tooltipIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
};

View File

@ -0,0 +1,207 @@
import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import { I18nContext } from '../../../contexts/i18n';
import Box from '../../ui/box';
import FormField from '../../ui/form-field';
import Typography from '../../ui/typography';
import {
ALIGN_ITEMS,
COLORS,
DISPLAY,
FLEX_DIRECTION,
TEXT_ALIGN,
FONT_WEIGHT,
TYPOGRAPHY,
JUSTIFY_CONTENT,
SIZES,
} from '../../../helpers/constants/design-system';
import { CustomSpendingCapTooltip } from './custom-spending-cap-tooltip';
export default function CustomSpendingCap({
tokenName,
currentTokenBalance,
dappProposedValue,
siteOrigin,
onEdit,
}) {
const t = useContext(I18nContext);
const [value, setValue] = useState('');
const [customSpendingCapText, setCustomSpendingCapText] = useState('');
const [error, setError] = useState('');
const inputLogicEmptyStateText = t('inputLogicEmptyState');
const getInputTextLogic = (inputNumber) => {
if (inputNumber <= currentTokenBalance) {
return {
className: 'custom-spending-cap__lowerValue',
description: t('inputLogicEqualOrSmallerNumber', [
<Typography
key="custom-spending-cap"
variant={TYPOGRAPHY.H6}
fontWeight={FONT_WEIGHT.BOLD}
className="custom-spending-cap__input-value-and-token-name"
>
{inputNumber} {tokenName}
</Typography>,
]),
};
} else if (inputNumber > currentTokenBalance) {
return {
className: 'custom-spending-cap__higherValue',
description: t('inputLogicHigherNumber'),
};
}
return {
className: 'custom-spending-cap__emptyState',
description: t('inputLogicEmptyState'),
};
};
const handleChange = (valueInput) => {
let spendingCapError = '';
const inputTextLogic = getInputTextLogic(valueInput);
const inputTextLogicDescription = inputTextLogic.description;
if (valueInput < 0 || isNaN(valueInput)) {
spendingCapError = t('spendingCapError');
setCustomSpendingCapText(t('spendingCapErrorDescription', [siteOrigin]));
setError(spendingCapError);
} else {
setCustomSpendingCapText(inputTextLogicDescription);
setError('');
}
setValue(valueInput);
};
const chooseTooltipContentText =
value > currentTokenBalance
? t('warningTooltipText', [
<Typography
key="tooltip-text"
variant={TYPOGRAPHY.H7}
fontWeight={FONT_WEIGHT.BOLD}
color={COLORS.ERROR_DEFAULT}
>
<i className="fa fa-exclamation-circle" /> {t('beCareful')}
</Typography>,
])
: t('inputLogicEmptyState');
return (
<>
<Box
textAlign={TEXT_ALIGN.END}
className="custom-spending-cap__max-button"
>
<button
className="custom-spending-cap__input--max-button"
type="link"
onClick={(e) => {
e.preventDefault();
handleChange(currentTokenBalance);
setValue(currentTokenBalance);
}}
>
{t('max')}
</button>
</Box>
<Box
className="custom-spending-cap"
borderRadius={SIZES.SM}
paddingTop={2}
paddingRight={6}
paddingLeft={6}
display={DISPLAY.FLEX}
alignItems={ALIGN_ITEMS.FLEX_START}
flexDirection={FLEX_DIRECTION.COLUMN}
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE}
gap={2}
>
<Box
justifyContent={JUSTIFY_CONTENT.CENTER}
display={DISPLAY.BLOCK}
className="custom-spending-cap__input"
>
<label
htmlFor={
value > (currentTokenBalance || error)
? 'custom-spending-cap-input-value'
: 'custom-spending-cap'
}
>
<FormField
dataTestId="custom-spending-cap-input"
autoFocus
wrappingLabelProps={{ as: 'div' }}
id={
value > (currentTokenBalance || error)
? 'custom-spending-cap-input-value'
: 'custom-spending-cap'
}
TooltipCustomComponent={
<CustomSpendingCapTooltip
tooltipContentText={value ? chooseTooltipContentText : ''}
tooltipIcon={value ? value > currentTokenBalance : ''}
/>
}
onChange={handleChange}
titleText={t('customSpendingCap')}
placeholder={t('enterANumber')}
error={error}
value={value}
titleDetail={
<button
className="custom-spending-cap__input--button"
type="link"
onClick={(e) => {
e.preventDefault();
if (value <= currentTokenBalance || error) {
handleChange(dappProposedValue);
setValue(dappProposedValue);
} else {
onEdit();
}
}}
>
{value > currentTokenBalance ? t('edit') : t('useDefault')}
</button>
}
titleDetailWrapperProps={{ marginBottom: 2, marginRight: 0 }}
/>
<Typography
color={COLORS.TEXT_DEFAULT}
variant={TYPOGRAPHY.H7}
boxProps={{ paddingTop: 2, paddingBottom: 2 }}
>
{value ? customSpendingCapText : inputLogicEmptyStateText}
</Typography>
</label>
</Box>
</Box>
</>
);
}
CustomSpendingCap.propTypes = {
/**
* Displayed the token name currently tracked in description related to the input state
*/
tokenName: PropTypes.string,
/**
* The current token balance of the token
*/
currentTokenBalance: PropTypes.number,
/**
* The dapp suggested amount
*/
dappProposedValue: PropTypes.number,
/**
* The origin of the site generally the URL
*/
siteOrigin: PropTypes.string,
/**
* onClick handler for the Edit link
*/
onEdit: PropTypes.func,
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import CustomSpendingCap from './custom-spending-cap';
export default {
title: 'Components/App/CustomSpendingCap',
id: __filename,
argTypes: {
tokenName: {
control: { type: 'text' },
},
currentTokenBalance: {
control: { type: 'number' },
},
dappProposedValue: {
control: { type: 'number' },
},
siteOrigin: {
control: { type: 'text' },
},
onEdit: {
action: 'onEdit',
},
},
args: {
tokenName: 'DAI',
currentTokenBalance: 200.12,
dappProposedValue: 7,
siteOrigin: 'Uniswap.org',
},
};
export const DefaultStory = (args) => {
return <CustomSpendingCap {...args} />;
};
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1 @@
export { default } from './custom-spending-cap';

View File

@ -0,0 +1,31 @@
.custom-spending-cap {
&__input-value-and-token-name {
display: contents;
}
&__max-button {
position: relative;
}
&__input {
width: 100%;
&--button {
background: none;
color: var(--color-primary-default);
}
&--max-button {
color: var(--color-text-alternative);
background: none;
position: absolute;
margin-top: 55px;
margin-inline-start: -75px;
}
}
#custom-spending-cap-input-value {
color: var(--color-error-default);
padding-inline-end: 60px;
}
}

View File

@ -208,9 +208,9 @@ FormField.propTypes = {
* Props to pass to wrapping Box component of the titleDetail component
* Accepts all props of the Box component
*/
titleDetailWrapperProps: {
...Box.PropTypes,
},
titleDetailWrapperProps: PropTypes.shape({
...Box.propTypes,
}),
/**
* Show error message
*/

View File

@ -1,6 +1,4 @@
.form-field {
margin-bottom: 20px;
&__heading {
display: flex;
align-items: center;
@ -16,6 +14,16 @@
align-self: center;
}
&__heading-title {
&__tooltip {
width: 180px;
&__warning-icon {
color: var(--color-error-default) !important;
}
}
}
&__error,
&__error h6 {
color: var(--color-error-default) !important;
@ -31,6 +39,7 @@
i {
color: var(--color-icon-default);
font-size: $font-size-h7;
margin-bottom: 10px;
}
&__input {