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:
parent
947f5299f8
commit
8e736e39d8
23
app/_locales/en/messages.json
generated
23
app/_locales/en/messages.json
generated
@ -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 don’t want the app to pull data from those those services."
|
||||
},
|
||||
"useDefault": {
|
||||
"message": "Use default"
|
||||
},
|
||||
"usePhishingDetection": {
|
||||
"message": "Use phishing detection"
|
||||
},
|
||||
|
@ -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';
|
||||
|
@ -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]),
|
||||
};
|
207
ui/components/app/custom-spending-cap/custom-spending-cap.js
Normal file
207
ui/components/app/custom-spending-cap/custom-spending-cap.js
Normal 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,
|
||||
};
|
@ -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';
|
1
ui/components/app/custom-spending-cap/index.js
Normal file
1
ui/components/app/custom-spending-cap/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './custom-spending-cap';
|
31
ui/components/app/custom-spending-cap/index.scss
Normal file
31
ui/components/app/custom-spending-cap/index.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user