Created a custom spending cap component (#15522)

feature/default_network_editable
VSaric 2 years ago committed by GitHub
parent 947f5299f8
commit 8e736e39d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      app/_locales/en/messages.json
  2. 1
      ui/components/app/app-components.scss
  3. 43
      ui/components/app/custom-spending-cap/custom-spending-cap-tooltip.js
  4. 207
      ui/components/app/custom-spending-cap/custom-spending-cap.js
  5. 36
      ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
  6. 1
      ui/components/app/custom-spending-cap/index.js
  7. 31
      ui/components/app/custom-spending-cap/index.scss
  8. 6
      ui/components/ui/form-field/form-field.js
  9. 13
      ui/components/ui/form-field/index.scss

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

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

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

@ -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…
Cancel
Save