diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b52cd4f7b0..6a4fe56efba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,14 +32,6 @@ jobs: echo "Working tree dirty at end of job" exit 1 fi - pr-linter: - runs-on: ubuntu-latest - steps: - # v5.2.0 - - name: Conventional Commit Validation PR Title - uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} scripts: runs-on: ubuntu-20.04 needs: setup @@ -77,6 +69,11 @@ jobs: - run: yarn test:unit --forceExit --silent env: NODE_OPTIONS: --max_old_space_size=20480 + - name: SonarCloud Scan + # v1.9.1 + uses: SonarSource/sonarcloud-github-action@5875562561d22a34be0c657405578705a169af6c + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - name: Require clean working directory shell: bash run: | @@ -84,12 +81,6 @@ jobs: echo "Working tree dirty at end of job" exit 1 fi - # Enable when team has access - # call-sonar-workflow: - # needs: tests - # uses: ./.github/workflows/sonar.yml - # secrets: - # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} check-workflows: name: Check workflows runs-on: ubuntu-latest @@ -105,6 +96,6 @@ jobs: all-jobs-pass: name: All jobs pass runs-on: ubuntu-20.04 - needs: [setup, scripts, tests, check-workflows, pr-linter] + needs: [setup, scripts, tests, check-workflows] steps: - run: echo "Great success!" diff --git a/.github/workflows/pr-title-linter.yml b/.github/workflows/pr-title-linter.yml new file mode 100644 index 00000000000..230ae583e91 --- /dev/null +++ b/.github/workflows/pr-title-linter.yml @@ -0,0 +1,15 @@ +name: "Conventional Commit Validation PR Title" +on: + pull_request: + branches: + - main + types: [opened,edited] + +jobs: + pr-title-linter: + runs-on: ubuntu-latest + steps: + # v5.2.0 + - uses: amannn/action-semantic-pull-request@c3cd5d1ea3580753008872425915e343e351ab54 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/remove_labels_after_pr_closed.yml b/.github/workflows/remove_labels_after_pr_closed.yml new file mode 100644 index 00000000000..68d8921c576 --- /dev/null +++ b/.github/workflows/remove_labels_after_pr_closed.yml @@ -0,0 +1,41 @@ +name: Remove labels after issue (or PR) closed + +on: + issues: + types: [closed] + pull_request: + types: [closed] + +jobs: + cleanup: + runs-on: ubuntu-latest + + steps: + - name: Remove labels + env: + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + run: | + LABELS=( + "product-backlog" + "needs-design" + "design-in-progress" + "ready-for-dev" + "sprint-backlog" + "in-progress" + "blocked" + "needs-dev-review" + "needs-qa" + "issues-found" + "ready-for-release" + ) + + for LABEL in "${LABELS[@]}"; do + curl \ + -X DELETE \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/$REPO/issues/$ISSUE_NUMBER/labels/$LABEL" + done diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml deleted file mode 100644 index 9ad35b09349..00000000000 --- a/.github/workflows/sonar.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Sonar -on: - workflow_call: - secrets: - SONAR_TOKEN: - required: true -jobs: - sonarcloud: - name: SonarCloud - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Shallow clones should be disabled for better relevancy of analysis - - name: SonarCloud Scan - # v1.9.1 - uses: SonarSource/sonarcloud-github-action@5875562561d22a34be0c657405578705a169af6c - with: - args: > - -Dsonar.javascript.lcov.reportPaths=tests/coverage/lcov.info - env: - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - \ No newline at end of file diff --git a/.github/workflows/stale-issue-pr.yml b/.github/workflows/stale-issue-pr.yml index 6a518482067..c21f33a654d 100644 --- a/.github/workflows/stale-issue-pr.yml +++ b/.github/workflows/stale-issue-pr.yml @@ -14,6 +14,7 @@ jobs: with: stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 7 days. Thank you for your contributions.' stale-issue-label: 'stale' + only-issue-labels: 'type-bug' exempt-issue-labels: 'type-security, type-pinned, feature-request, awaiting-metamask' stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity in the last 90 days. It will be closed in 7 days. Thank you for your contributions.' stale-pr-label: 'stale' @@ -24,4 +25,5 @@ jobs: days-before-pr-stale: 90 days-before-issue-close: 7 days-before-pr-close: 7 + operations-per-run: 200 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b657fd1158..fd75cfb1435 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ - [#6228](https://github.com/MetaMask/metamask-mobile/pull/6228): [UPDATE] Checkbox component - [#6226](https://github.com/MetaMask/metamask-mobile/pull/6226): [UPDATE] Button's icon props and button org + ## 6.5.0 - May 4, 2023 - [#5743](https://github.com/MetaMask/metamask-mobile/pull/5743): [FEATURE] On-ramp: Add buy-crypto deeplink - [#6201](https://github.com/MetaMask/metamask-mobile/pull/6201): [FIX] [SDK] Missing redirect breaking backward compatibility diff --git a/Gemfile.lock b/Gemfile.lock index 98d0bfa7800..af2467ee31f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,7 @@ GEM nap (1.1.0) netrc (0.11.0) public_suffix (4.0.7) - rexml (3.2.4) + rexml (3.2.5) ruby-macho (2.5.1) typhoeus (1.4.0) ethon (>= 0.9.0) @@ -87,6 +87,7 @@ GEM PLATFORMS arm64-darwin-22 + x86_64-linux DEPENDENCIES cocoapods (>= 1.11.3) diff --git a/app/actions/modals/index.js b/app/actions/modals/index.js index e4522617650..c3c3f06681d 100644 --- a/app/actions/modals/index.js +++ b/app/actions/modals/index.js @@ -26,13 +26,6 @@ export function toggleDappTransactionModal(show) { }; } -export function toggleApproveModal(show) { - return { - type: 'TOGGLE_APPROVE_MODAL', - show, - }; -} - export function toggleInfoNetworkModal(show) { return { type: 'TOGGLE_INFO_NETWORK_MODAL', diff --git a/app/actions/transaction/index.js b/app/actions/transaction/index.js index c5ca921056d..26dc08621d1 100644 --- a/app/actions/transaction/index.js +++ b/app/actions/transaction/index.js @@ -85,36 +85,6 @@ export function prepareTransaction(transaction) { }; } -/** - * Sets transaction object to be sent. All properties can be updated - * - * @param {object} config - * @param {object} config.transaction - Transaction object with from, to, data, gas, gasPrice, value - * @param {string} config.ensRecipient - Resolved ens name to send the transaction to - * @param {string} config.transactionToName - Resolved address book name for to address - * @param {string} config.transactionFromName - Resolved address book name for from address - * @param {object} config.selectedAsset - Asset to start the transaction with - * @param {string} config.assetType - The selectedAsset's type - */ -export function prepareFullTransaction({ - transaction, - ensRecipient, - transactionToName, - transactionFromName, - selectedAsset, - assetType, -}) { - return { - type: 'PREPARE_FULL_TRANSACTION', - transaction, - ensRecipient, - transactionToName, - transactionFromName, - selectedAsset, - assetType, - }; -} - /** * Sets any attribute in transaction object * diff --git a/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx b/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx index 97f21b38438..55e59a63224 100644 --- a/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx +++ b/app/component-library/components-temp/Accounts/AccountBalance/AccountBalance.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { AccountBalanceProps } from './AccountBalance.types'; + import Card from '../../../components/Cards/Card'; import AccountBase from '../AccountBase/AccountBase'; import { ACCOUNT_BALANCE_TEST_ID } from './AccountBalance.constants'; import styles from './AccountBalance.styles'; +import { AccountBalanceProps } from './AccountBalance.types'; const AccountBalance = ({ accountBalance, diff --git a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx index 7073e02dbce..9b8daa114fa 100644 --- a/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx +++ b/app/component-library/components-temp/Accounts/AccountBase/AccountBase.tsx @@ -1,16 +1,17 @@ import React from 'react'; import { View } from 'react-native'; -import { AccountBaseProps } from './AccountBase.types'; -import Text, { TextVariant } from '../../../components/Texts/Text'; -import BadgeWrapper from '../../../components/Badges/BadgeWrapper'; + import Badge from '../../../../component-library/components/Badges/Badge'; import Avatar, { AvatarVariants } from '../../../components/Avatars/Avatar'; import { AvatarAccountType } from '../../../components/Avatars/Avatar/variants/AvatarAccount'; +import BadgeWrapper from '../../../components/Badges/BadgeWrapper'; +import Text, { TextVariant } from '../../../components/Texts/Text'; import { ACCOUNT_BALANCE_AVATAR_TEST_ID, ACCOUNT_BASE_TEST_ID, } from './AccountBase.constants'; import styles from './AccountBase.styles'; +import { AccountBaseProps } from './AccountBase.types'; const AccountBase = ({ accountBalance, diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts index 05c7a0d961e..237d3db4b58 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts +++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.styles.ts @@ -20,6 +20,9 @@ const styleSheet = (params: { theme: Theme }) => { flexDirection: 'row', justifyContent: 'space-between', }, + fixedPadding: { + padding: 0, + }, body: { flexDirection: 'row', flex: 1, diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx index bf9258c235c..d5548f0386b 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx +++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.test.tsx @@ -1,10 +1,9 @@ +import { shallow } from 'enzyme'; // Third party dependencies. import React from 'react'; -import { shallow } from 'enzyme'; // External dependencies. import { TICKER } from '../CustomSpendCap.constants'; - // Internal dependencies. import CustomInput from './CustomInput'; import { CustomInputProps } from './CustomInput.types'; @@ -16,9 +15,9 @@ describe('CustomInput', () => { props = { ticker: TICKER, value: '123', - inputDisabled: true, + isInputGreaterThanBalance: false, + isEditDisabled: false, setMaxSelected: jest.fn(), - defaultValueSelected: true, setValue: jest.fn(), }; }); diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx index 32259ac5420..3283f32a7ff 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx +++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/CustomInput.tsx @@ -2,24 +2,23 @@ import React from 'react'; import { TextInput, View } from 'react-native'; -// External dependencies. -import { useStyles } from '../../../hooks'; -import formatNumber from '../../../../util/formatNumber'; import { strings } from '../../../../../locales/i18n'; +import formatNumber from '../../../../util/formatNumber'; import Text, { TextVariant } from '../../../components/Texts/Text'; - -// Internal dependencies. -import { CustomInputProps } from './CustomInput.types'; +// External dependencies. +import { useStyles } from '../../../hooks'; import CUSTOM_INPUT_TEST_ID from './CustomInput.constants'; import stylesheet from './CustomInput.styles'; +// Internal dependencies. +import { CustomInputProps } from './CustomInput.types'; const CustomInput = ({ ticker, value, - inputDisabled, setMaxSelected, - defaultValueSelected, + isInputGreaterThanBalance, setValue, + isEditDisabled, }: CustomInputProps) => { const handleUpdate = (text: string) => { setValue(text); @@ -29,7 +28,10 @@ const CustomInput = ({ setMaxSelected(true); }; - const { styles } = useStyles(stylesheet, {}); + const { + styles, + theme: { colors }, + } = useStyles(stylesheet, {}); const onChangeValueText = (text: string) => { handleUpdate(text); @@ -37,25 +39,39 @@ const CustomInput = ({ }; return ( - + - {inputDisabled ? ( + {!isEditDisabled ? ( - ) : defaultValueSelected ? ( + ) : ( {`${formatNumber(value)} ${ticker}`} - ) : null} + )} - {inputDisabled && ( + {!isEditDisabled && ( void; /** - * Boolean to determine if the input is disabled + * Boolean to determine if input is greater than balance * @default false */ - inputDisabled?: boolean; - /** - * Boolean to determine if default value is selected - * @default false - */ - defaultValueSelected: boolean; + isInputGreaterThanBalance: boolean; /** * Function to update max state */ setMaxSelected: (value: boolean) => void; + /** + * Boolean to disable edit + */ + isEditDisabled: boolean; } diff --git a/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap b/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap index 4c8d6c2ddff..e39d127775b 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap +++ b/app/component-library/components-temp/CustomSpendCap/CustomInput/__snapshots__/CustomInput.test.tsx.snap @@ -3,13 +3,16 @@ exports[`CustomInput should render correctly 1`] = ` @@ -26,20 +29,23 @@ exports[`CustomInput should render correctly 1`] = ` keyboardType="numeric" multiline={true} onChangeText={[Function]} - placeholder="Enter a number here (DAI)" + placeholder="Enter a number here" style={ - Object { - "color": "#24272A", - "flexGrow": 1, - "fontFamily": "Euclid Circular B", - "fontSize": 14, - "fontWeight": "400", - "letterSpacing": 0, - "lineHeight": 22, - "marginRight": 16, - "paddingBottom": 0, - "paddingTop": 0, - } + Array [ + Object { + "color": "#24272A", + "flexGrow": 1, + "fontFamily": "Euclid Circular B", + "fontSize": 14, + "fontWeight": "400", + "letterSpacing": 0, + "lineHeight": 22, + "marginRight": 16, + "paddingBottom": 0, + "paddingTop": 0, + }, + false, + ] } value="123" /> diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx index 8f8c4f1aa48..3f8b4d962c5 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx +++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.stories.tsx @@ -1,16 +1,17 @@ // Third party dependencies. import React from 'react'; + import { storiesOf } from '@storybook/react-native'; +import CustomSpendCap from './CustomSpendCap'; // Internal dependencies. import { - TICKER, ACCOUNT_BALANCE, - DAPP_PROPOSED_VALUE, DAPP_DOMAIN, + DAPP_PROPOSED_VALUE, INPUT_VALUE_CHANGED, + TICKER, } from './CustomSpendCap.constants'; -import CustomSpendCap from './CustomSpendCap'; storiesOf('Component Library / CustomSpendCap', module).add('Default', () => ( ( dappProposedValue={DAPP_PROPOSED_VALUE} domain={DAPP_DOMAIN} onInputChanged={INPUT_VALUE_CHANGED} + isEditDisabled={false} + editValue={() => undefined} + tokenSpendValue={''} /> )); diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx index a2eecfecf16..0a564f89879 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx +++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.test.tsx @@ -1,32 +1,106 @@ +import { shallow } from 'enzyme'; // Third party dependencies. import React from 'react'; -import { shallow } from 'enzyme'; -// Internal dependencies. -import { CustomSpendCapProps } from './CustomSpendCap.types'; +import renderWithProvider from '../../../util/test/renderWithProvider'; import CustomSpendCap from './CustomSpendCap'; import { - TICKER, ACCOUNT_BALANCE, - DAPP_PROPOSED_VALUE, + CUSTOM_SPEND_CAP_TEST_ID, DAPP_DOMAIN, + DAPP_PROPOSED_VALUE, INPUT_VALUE_CHANGED, + TICKER, } from './CustomSpendCap.constants'; +// Internal dependencies. +import { CustomSpendCapProps } from './CustomSpendCap.types'; + +function RenderCustomSpendCap( + tokenSpendValue: string, + isInputValid: () => boolean = () => true, +) { + return ( + ({})} + tokenSpendValue={tokenSpendValue} + isInputValid={isInputValid} + /> + ); +} + +const isInputValid = jest.fn(); describe('CustomSpendCap', () => { it('should render CustomSpendCap', () => { - const wrapper = shallow( - , - ); + const wrapper = shallow(RenderCustomSpendCap('')); const singleSelectComponent = wrapper.findWhere( - (node) => node.prop('testID') === 'custom-spend-cap', + (node) => node.prop('testID') === CUSTOM_SPEND_CAP_TEST_ID, ); expect(singleSelectComponent.exists()).toBe(true); }); + + it('should match snapshot', () => { + const container = renderWithProvider(RenderCustomSpendCap('')); + expect(container).toMatchSnapshot(); + }); + + it('should render error message is value is not a number', async () => { + const notANumber = 'abc'; + const { findByText } = renderWithProvider(RenderCustomSpendCap(notANumber)); + + expect(await findByText('Error: Enter only numbers')).toBeDefined(); + }); + + it('should render valid message if value is 0', async () => { + const zeroValue = '0'; + const { findByText } = renderWithProvider(RenderCustomSpendCap(zeroValue)); + + expect( + await findByText( + `Only enter a number that you're comfortable with ${DAPP_DOMAIN} accessing now or in the future. You can always increase the token limit later.`, + ), + ).toBeDefined(); + }); + + it('should render valid message if value is less than or equal to account balance', async () => { + const valueLessThanBalance = '100'; + const { toJSON } = renderWithProvider( + RenderCustomSpendCap(valueLessThanBalance), + ); + + expect(JSON.stringify(toJSON())).toMatch( + `${valueLessThanBalance} ${TICKER}`, + ); + }); + + it('should render valid message if value is greater than account balance', async () => { + const valueGreaterThanBalance = '300'; + const valueDifference = + Number(valueGreaterThanBalance) - Number(ACCOUNT_BALANCE); + const { toJSON } = renderWithProvider( + RenderCustomSpendCap(valueGreaterThanBalance), + ); + + expect(JSON.stringify(toJSON())).toMatch(`${valueDifference} ${TICKER}`); + }); + + it('should call isInputValid with false if value is not a number', async () => { + const notANumber = 'abc'; + renderWithProvider(RenderCustomSpendCap(notANumber, isInputValid)); + + expect(isInputValid).toHaveBeenCalledWith(false); + }); + + it('should call isInputValid with true if value is a number', async () => { + const validNumber = '100'; + renderWithProvider(RenderCustomSpendCap(validNumber, isInputValid)); + + expect(isInputValid).toHaveBeenCalledWith(true); + }); }); diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx index e2412868b0c..a49675af402 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx +++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.tsx @@ -1,23 +1,22 @@ +import BigNumber from 'bignumber.js'; // Third party dependencies. -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Pressable, View } from 'react-native'; -import BigNumber from 'bignumber.js'; -// External dependencies. -import { useStyles } from '../../hooks'; import { strings } from '../../../../locales/i18n'; -import Button, { ButtonVariants } from '../../components/Buttons/Button'; -import Text, { TextVariant } from '../../components/Texts/Text'; +import InfoModal from '../../../components/UI/Swaps/components/InfoModal'; import formatNumber from '../../../util/formatNumber'; import { isNumber } from '../../../util/number'; +import Button, { ButtonVariants } from '../../components/Buttons/Button'; +import Icon, { IconName, IconSize } from '../../components/Icons/Icon'; +import Text, { TextVariant } from '../../components/Texts/Text'; +// External dependencies. +import { useStyles } from '../../hooks'; import CustomInput from './CustomInput'; -import InfoModal from '../../../components/UI/Swaps/components/InfoModal'; - // Internal dependencies. import { CUSTOM_SPEND_CAP_TEST_ID } from './CustomSpendCap.constants'; -import { CustomSpendCapProps } from './CustomSpendCap.types'; import customSpendCapStyles from './CustomSpendCap.styles'; -import Icon, { IconName, IconSize } from '../../components/Icons/Icon'; +import { CustomSpendCapProps } from './CustomSpendCap.types'; const CustomSpendCap = ({ ticker, @@ -25,16 +24,19 @@ const CustomSpendCap = ({ accountBalance, domain, onInputChanged, + isEditDisabled, + editValue, + tokenSpendValue, + isInputValid, }: CustomSpendCapProps) => { const { styles, theme: { colors }, } = useStyles(customSpendCapStyles, {}); - const [value, setValue] = useState(''); + const [value, setValue] = useState(tokenSpendValue); const [inputDisabled, setInputDisabled] = useState(true); const [maxSelected, setMaxSelected] = useState(false); - const [defaultValueSelected, setDefaultValueSelected] = useState(false); const [ inputValueHigherThanAccountBalance, setInputValueHigherThanAccountBalance, @@ -45,26 +47,34 @@ const CustomSpendCap = ({ useEffect(() => { if (isNumber(value)) { setInputHasError(false); - return onInputChanged(value); + } else { + setInputHasError(true); } - return setInputHasError(true); + + onInputChanged(value); }, [value, onInputChanged]); - const handlePress = () => { + useEffect(() => { + isInputValid(!inputHasError); + }, [inputHasError, isInputValid]); + + const handleDefaultValue = () => { setMaxSelected(false); setValue(dappProposedValue); - setDefaultValueSelected(!defaultValueSelected); setInputDisabled(!inputDisabled); }; + const handlePress = () => { + if (isEditDisabled) editValue(); + handleDefaultValue(); + }; + useEffect(() => { if (maxSelected) setValue(accountBalance); }, [maxSelected, accountBalance]); - const editedDefaultValue = new BigNumber(dappProposedValue); const newValue = new BigNumber(value); - const dappValue = editedDefaultValue.minus(accountBalance).toFixed(); const difference = newValue.minus(accountBalance).toFixed(); useEffect(() => { @@ -88,7 +98,7 @@ const CustomSpendCap = ({ { domain }, ); - const DAPP_PROPOSED_VALUE_GREATER_THAN_ACCOUNT_BALANCE = ( + const INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE = ( <> {strings('contract_allowance.custom_spend_cap.this_contract_allows')} @@ -96,23 +106,19 @@ const CustomSpendCap = ({ {strings('contract_allowance.custom_spend_cap.from_your_current_balance')} - {` ${formatNumber(dappValue)} ${ticker} `} + {` ${formatNumber(difference)} ${ticker} `} {strings('contract_allowance.custom_spend_cap.future_tokens')} ); - const INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE = ( + const INPUT_VALUE_LOWER_THAN_ACCOUNT_BALANCE = ( <> {strings('contract_allowance.custom_spend_cap.this_contract_allows')} - {` ${formatNumber(accountBalance)} ${ticker} `} - - {strings('contract_allowance.custom_spend_cap.from_your_current_balance')} - - {` ${formatNumber(difference)} ${ticker} `} + {` ${formatNumber(tokenSpendValue ?? '0')} ${ticker} `} - {strings('contract_allowance.custom_spend_cap.future_tokens')} + {strings('contract_allowance.custom_spend_cap.from_your_balance')} ); @@ -120,7 +126,7 @@ const CustomSpendCap = ({ setIsModalVisible(!isModalVisible); }; - const infoModalTitle = defaultValueSelected ? ( + const infoModalTitle = inputValueHigherThanAccountBalance ? ( <> ); + let message; + + if (!value || !Number(value)) { + message = NO_SELECTED; + } else if (maxSelected) { + message = MAX_VALUE_SELECTED; + } else if (inputValueHigherThanAccountBalance) { + message = INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE; + } else { + message = INPUT_VALUE_LOWER_THAN_ACCOUNT_BALANCE; + } + return ( {isModalVisible ? ( @@ -145,7 +163,7 @@ const CustomSpendCap = ({ title={infoModalTitle} body={ - {defaultValueSelected + {inputValueHigherThanAccountBalance ? strings( 'contract_allowance.custom_spend_cap.info_modal_description_default', ) @@ -168,9 +186,15 @@ const CustomSpendCap = ({ @@ -180,7 +204,7 @@ const CustomSpendCap = ({ onPress={handlePress} textVariant={TextVariant.BodyMD} label={ - defaultValueSelected + isEditDisabled ? strings('contract_allowance.custom_spend_cap.edit') : strings('contract_allowance.custom_spend_cap.use_default') } @@ -190,10 +214,10 @@ const CustomSpendCap = ({ {value.length > 0 && inputHasError && ( @@ -201,17 +225,13 @@ const CustomSpendCap = ({ {strings('contract_allowance.custom_spend_cap.error_enter_number')} )} - - - {defaultValueSelected - ? DAPP_PROPOSED_VALUE_GREATER_THAN_ACCOUNT_BALANCE - : maxSelected - ? MAX_VALUE_SELECTED - : inputValueHigherThanAccountBalance - ? INPUT_VALUE_GREATER_THAN_ACCOUNT_BALANCE - : NO_SELECTED} - - + {!isEditDisabled && ( + + + {message} + + + )} ); }; diff --git a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts index 0f94d92c2e3..8b4172c9ba1 100644 --- a/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts +++ b/app/component-library/components-temp/CustomSpendCap/CustomSpendCap.types.ts @@ -7,4 +7,21 @@ export interface CustomSpendCapProps { * @param value - The value of the input field */ onInputChanged: (value: string) => void; + /** + * isEditDisabled - Boolean to disable edit + * @default false + */ + isEditDisabled: boolean; + /** + * function to return to input field + */ + editValue: () => void; + /** + * token spend value - The value of the input field + */ + tokenSpendValue: string; + /** + * isInputValid - function to check if input is valid and has no errors + */ + isInputValid: (value: boolean) => boolean; } diff --git a/app/component-library/components-temp/CustomSpendCap/__snapshots__/CustomSpendCap.test.tsx.snap b/app/component-library/components-temp/CustomSpendCap/__snapshots__/CustomSpendCap.test.tsx.snap new file mode 100644 index 00000000000..7b6419829be --- /dev/null +++ b/app/component-library/components-temp/CustomSpendCap/__snapshots__/CustomSpendCap.test.tsx.snap @@ -0,0 +1,186 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CustomSpendCap should match snapshot 1`] = ` + + + + + Custom spending cap + + + + + + + Use default + + + + + + + + + Max + + + + + + Only enter a number that you're comfortable with Uniswap.org accessing now or in the future. You can always increase the token limit later. + + + +`; diff --git a/app/component-library/components/Banners/Banner/Banner.test.tsx b/app/component-library/components/Banners/Banner/Banner.test.tsx new file mode 100644 index 00000000000..94ba52874eb --- /dev/null +++ b/app/component-library/components/Banners/Banner/Banner.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; + +import { render } from '@testing-library/react-native'; +import Banner from './Banner'; +import Text from '../../Texts/Text/Text'; +import { BannerAlertSeverity } from './variants/BannerAlert/BannerAlert.types'; +import { BannerVariant } from './Banner.types'; +import { ButtonVariants } from '../../Buttons/Button'; +import { IconName } from '../../Icons/Icon'; +import { ButtonIconSizes, ButtonIconVariants } from '../../Buttons/ButtonIcon'; +import { TESTID_BANNER_CLOSE_BUTTON_ICON } from './foundation/BannerBase/BannerBase.constants'; + +describe('Banner', () => { + it('should render correctly', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render correctly with a start accessory', async () => { + const wrapper = render( + Test Start accessory} + />, + ); + + expect(wrapper).toMatchSnapshot(); + expect(await wrapper.findByText('Test Start accessory')).toBeDefined(); + }); + + it('should render correctly with an action button', async () => { + const wrapper = render( + jest.fn(), + variant: ButtonVariants.Secondary, + }} + />, + ); + + expect(wrapper).toMatchSnapshot(); + expect(await wrapper.findByText('Test Action Button')).toBeDefined(); + }); + + it('should render correctly with a close button', async () => { + const wrapper = render( + jest.fn(), + variant: ButtonVariants.Secondary, + }} + closeButtonProps={{ + onPress: () => jest.fn(), + iconName: IconName.Close, + variant: ButtonIconVariants.Primary, + size: ButtonIconSizes.Sm, + }} + />, + ); + + expect(wrapper).toMatchSnapshot(); + expect(await wrapper.findByText('Test Action Button')).toBeDefined(); + expect( + await wrapper.queryByTestId(TESTID_BANNER_CLOSE_BUTTON_ICON), + ).toBeDefined(); + }); +}); diff --git a/app/component-library/components/Banners/Banner/README.md b/app/component-library/components/Banners/Banner/README.md index a2e2beeeeab..a8e5ce7e2dd 100644 --- a/app/component-library/components/Banners/Banner/README.md +++ b/app/component-library/components/Banners/Banner/README.md @@ -114,6 +114,14 @@ Optional prop to control the close button's props. | :-------------------------------------------------- | :------------------------------------------------------ | | [ButtonIconProps](../../../../Buttons/ButtonIcon/ButtonIcon.types.ts) | No | +### `children` + +Optional prop to add children components to the Banner + +| TYPE | REQUIRED | +| :-------------------------------------------------- | :------------------------------------------------------ | +| React.ReactNode | No | + ## Usage ```javascript diff --git a/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap b/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap new file mode 100644 index 00000000000..11e7d17f6e3 --- /dev/null +++ b/app/component-library/components/Banners/Banner/__snapshots__/Banner.test.tsx.snap @@ -0,0 +1,412 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Banner should render correctly 1`] = ` + + + + + + + Hello Error Banner World + + + This is nothing but a test of the emergency broadcast system. + + + +`; + +exports[`Banner should render correctly with a close button 1`] = ` + + + + + + + Hello Error Banner World + + + This is nothing but a test of the emergency broadcast system. + + + + Test Action Button + + + + + + + + + +`; + +exports[`Banner should render correctly with a start accessory 1`] = ` + + + + Test Start accessory + + + + + Hello Error Banner World + + + This is nothing but a test of the emergency broadcast system. + + + +`; + +exports[`Banner should render correctly with an action button 1`] = ` + + + + + + + Hello Error Banner World + + + This is nothing but a test of the emergency broadcast system. + + + + Test Action Button + + + + +`; diff --git a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx index bf13358249d..b1ae89b3eaa 100644 --- a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx +++ b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.constants.tsx @@ -17,6 +17,9 @@ import { SAMPLE_ICON_PROPS } from '../../../../Icons/Icon/Icon.constants'; // Internal dependencies. import { BannerBaseProps } from './BannerBase.types'; +// Test IDs +export const TESTID_BANNER_CLOSE_BUTTON_ICON = 'banner-close-button-icon'; + // Defaults export const DEFAULT_BANNERBASE_TITLE_TEXTVARIANT = TextVariant.BodyLGMedium; export const DEFAULT_BANNERBASE_DESCRIPTION_TEXTVARIANT = TextVariant.BodyMD; diff --git a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx index 3e2954266b9..ea46b42f796 100644 --- a/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx +++ b/app/component-library/components/Banners/Banner/foundation/BannerBase/BannerBase.tsx @@ -22,6 +22,7 @@ import { DEFAULT_BANNERBASE_CLOSEBUTTON_BUTTONICONVARIANT, DEFAULT_BANNERBASE_CLOSEBUTTON_BUTTONICONSIZE, DEFAULT_BANNERBASE_CLOSEBUTTON_ICONNAME, + TESTID_BANNER_CLOSE_BUTTON_ICON, } from './BannerBase.constants'; const BannerBase: React.FC = ({ @@ -32,6 +33,7 @@ const BannerBase: React.FC = ({ actionButtonProps, onClose, closeButtonProps, + children, ...props }) => { const { styles } = useStyles(styleSheet, { style }); @@ -63,10 +65,12 @@ const BannerBase: React.FC = ({ {...actionButtonProps} /> )} + {children} {(onClose || closeButtonProps) && ( { + console.log('Cancel button clicked'); + }, + }, + { + variant: ButtonVariants.Primary, + label: 'Submit', + onPress: () => { + console.log('Submit button clicked'); + }, + }, + ], +}; diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx new file mode 100644 index 00000000000..d3548deb6ae --- /dev/null +++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.stories.tsx @@ -0,0 +1,41 @@ +/* eslint-disable no-console */ + +// Third party dependencies. +import React from 'react'; +import { storiesOf } from '@storybook/react-native'; +import { select } from '@storybook/addon-knobs'; + +// External dependencies. +import { storybookPropsGroupID } from '../../../constants/storybook.constants'; + +// Internal dependencies. +import BottomSheetFooter from './BottomSheetFooter'; +import { + BottomSheetFooterProps, + ButtonsAlignment, +} from './BottomSheetFooter.types'; +import { + DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT, + SAMPLE_BOTTOMSHEETFOOTER_PROPS, +} from './BottomSheetFooter.constants'; + +export const getBottomSheetFooterStoryProps = (): BottomSheetFooterProps => ({ + buttonsAlignment: select( + 'buttonsAlignment', + ButtonsAlignment, + DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT, + storybookPropsGroupID, + ), + buttonPropsArray: SAMPLE_BOTTOMSHEETFOOTER_PROPS.buttonPropsArray, +}); + +const BottomSheetFooterStory = () => ( + +); + +storiesOf('Component Library / BottomSheets', module).add( + 'BottomSheetFooter', + BottomSheetFooterStory, +); + +export default BottomSheetFooterStory; diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.styles.ts b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.styles.ts new file mode 100644 index 00000000000..6754be11ffe --- /dev/null +++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.styles.ts @@ -0,0 +1,54 @@ +// Third party dependencies. +import { StyleSheet, ViewStyle } from 'react-native'; + +// External dependencies. +import { Theme } from '../../../../util/theme/models'; + +// Internal dependencies. +import { + ButtonsAlignment, + BottomSheetFooterStyleSheetVars, +} from './BottomSheetFooter.types'; + +/** + * Style sheet function for BottomSheetFooter component. + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { + theme: Theme; + vars: BottomSheetFooterStyleSheetVars; +}) => { + const { vars, theme } = params; + const { style, buttonsAlignment } = vars; + const buttonStyle: ViewStyle = + buttonsAlignment === ButtonsAlignment.Horizontal + ? { flex: 1 } + : { alignSelf: 'stretch' }; + + return StyleSheet.create({ + base: Object.assign( + { + backgroundColor: theme.colors.background.default, + flexDirection: + buttonsAlignment === ButtonsAlignment.Horizontal ? 'row' : 'column', + paddingVertical: 4, + paddingHorizontal: 8, + } as ViewStyle, + style, + ) as ViewStyle, + button: { + ...buttonStyle, + }, + subsequentButton: { + ...buttonStyle, + marginLeft: buttonsAlignment === ButtonsAlignment.Horizontal ? 16 : 0, + marginTop: buttonsAlignment === ButtonsAlignment.Vertical ? 16 : 0, + }, + }); +}; + +export default styleSheet; diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.test.tsx b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.test.tsx new file mode 100644 index 00000000000..ed1d3a3b37f --- /dev/null +++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.test.tsx @@ -0,0 +1,57 @@ +// Third party dependencies. +import React from 'react'; +import { render } from '@testing-library/react-native'; + +// Internal dependencies. +import BottomSheetFooter from './BottomSheetFooter'; +import { + SAMPLE_BOTTOMSHEETFOOTER_PROPS, + TESTID_BOTTOMSHEETFOOTER, + TESTID_BOTTOMSHEETFOOTER_BUTTON, + TESTID_BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT, +} from './BottomSheetFooter.constants'; +import { ButtonsAlignment } from './BottomSheetFooter.types'; + +describe('BottomSheetFooter', () => { + it('should render snapshot correctly', () => { + const wrapper = render( + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render the correct default buttonsAlignment', () => { + const { getByTestId } = render( + , + ); + expect( + getByTestId(TESTID_BOTTOMSHEETFOOTER).props.style.flexDirection, + ).toBe('row'); + }); + + it('should render the correct given buttonsAlignment', () => { + const givenButtonsAlignment = ButtonsAlignment.Vertical; + const { getByTestId } = render( + , + ); + expect( + getByTestId(TESTID_BOTTOMSHEETFOOTER).props.style.flexDirection, + ).toBe('column'); + }); + + it('should render the correct gap between buttons', () => { + const { getByTestId } = render( + , + ); + expect( + getByTestId(TESTID_BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT).props.style + .marginLeft, + ).toBe(16); + expect( + getByTestId(TESTID_BOTTOMSHEETFOOTER_BUTTON).props.style.marginLeft, + ).not.toBe(16); + }); +}); diff --git a/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.tsx b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.tsx new file mode 100644 index 00000000000..6848ae54800 --- /dev/null +++ b/app/component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter.tsx @@ -0,0 +1,46 @@ +/* eslint-disable react/prop-types */ + +// Third party dependencies. +import React from 'react'; +import { View } from 'react-native'; + +// External dependencies. +import { useStyles } from '../../../hooks'; +import Button from '../../Buttons/Button'; + +// Internal dependencies. +import styleSheet from './BottomSheetFooter.styles'; +import { BottomSheetFooterProps } from './BottomSheetFooter.types'; +import { + DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT, + TESTID_BOTTOMSHEETFOOTER, + TESTID_BOTTOMSHEETFOOTER_BUTTON, + TESTID_BOTTOMSHEETFOOTER_BUTTON_SUBSEQUENT, +} from './BottomSheetFooter.constants'; + +const BottomSheetFooter: React.FC = ({ + style, + buttonsAlignment = DEFAULT_BOTTOMSHEETFOOTER_BUTTONSALIGNMENT, + buttonPropsArray, +}) => { + const { styles } = useStyles(styleSheet, { style, buttonsAlignment }); + + return ( + + {buttonPropsArray.map((buttonProp, index) => ( +