From 8e736e39d86e06a0fe3922e0256c02223b71dfd8 Mon Sep 17 00:00:00 2001
From: VSaric <92527393+VSaric@users.noreply.github.com>
Date: Wed, 28 Sep 2022 21:46:29 +0200
Subject: [PATCH] Created a custom spending cap component (#15522)
---
app/_locales/en/messages.json | 23 ++
ui/components/app/app-components.scss | 1 +
.../custom-spending-cap-tooltip.js | 43 ++++
.../custom-spending-cap.js | 207 ++++++++++++++++++
.../custom-spending-cap.stories.js | 36 +++
.../app/custom-spending-cap/index.js | 1 +
.../app/custom-spending-cap/index.scss | 31 +++
ui/components/ui/form-field/form-field.js | 6 +-
ui/components/ui/form-field/index.scss | 13 +-
9 files changed, 356 insertions(+), 5 deletions(-)
create mode 100644 ui/components/app/custom-spending-cap/custom-spending-cap-tooltip.js
create mode 100644 ui/components/app/custom-spending-cap/custom-spending-cap.js
create mode 100644 ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
create mode 100644 ui/components/app/custom-spending-cap/index.js
create mode 100644 ui/components/app/custom-spending-cap/index.scss
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 61c586fed..a1e4a1f92 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -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"
},
diff --git a/ui/components/app/app-components.scss b/ui/components/app/app-components.scss
index dc85fc6a1..fd9813522 100644
--- a/ui/components/app/app-components.scss
+++ b/ui/components/app/app-components.scss
@@ -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';
diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap-tooltip.js b/ui/components/app/custom-spending-cap/custom-spending-cap-tooltip.js
new file mode 100644
index 000000000..438b66d01
--- /dev/null
+++ b/ui/components/app/custom-spending-cap/custom-spending-cap-tooltip.js
@@ -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,
+}) => (
+
+
+ {tooltipContentText}
+
+ }
+ >
+ {tooltipIcon ? (
+
+ ) : (
+ tooltipIcon !== '' &&
+ )}
+
+
+);
+
+CustomSpendingCapTooltip.propTypes = {
+ tooltipContentText: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ tooltipIcon: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
+};
diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.js b/ui/components/app/custom-spending-cap/custom-spending-cap.js
new file mode 100644
index 000000000..b2b4d9d01
--- /dev/null
+++ b/ui/components/app/custom-spending-cap/custom-spending-cap.js
@@ -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', [
+
+ {inputNumber} {tokenName}
+ ,
+ ]),
+ };
+ } 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', [
+
+ {t('beCareful')}
+ ,
+ ])
+ : t('inputLogicEmptyState');
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+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,
+};
diff --git a/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
new file mode 100644
index 000000000..a8f757926
--- /dev/null
+++ b/ui/components/app/custom-spending-cap/custom-spending-cap.stories.js
@@ -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 ;
+};
+
+DefaultStory.storyName = 'Default';
diff --git a/ui/components/app/custom-spending-cap/index.js b/ui/components/app/custom-spending-cap/index.js
new file mode 100644
index 000000000..de5e462b6
--- /dev/null
+++ b/ui/components/app/custom-spending-cap/index.js
@@ -0,0 +1 @@
+export { default } from './custom-spending-cap';
diff --git a/ui/components/app/custom-spending-cap/index.scss b/ui/components/app/custom-spending-cap/index.scss
new file mode 100644
index 000000000..00ae12252
--- /dev/null
+++ b/ui/components/app/custom-spending-cap/index.scss
@@ -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;
+ }
+}
diff --git a/ui/components/ui/form-field/form-field.js b/ui/components/ui/form-field/form-field.js
index 48739a958..9cbd9137e 100644
--- a/ui/components/ui/form-field/form-field.js
+++ b/ui/components/ui/form-field/form-field.js
@@ -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
*/
diff --git a/ui/components/ui/form-field/index.scss b/ui/components/ui/form-field/index.scss
index 224d82ad4..a1c267285 100644
--- a/ui/components/ui/form-field/index.scss
+++ b/ui/components/ui/form-field/index.scss
@@ -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 {