commit
74719a8102
@ -0,0 +1,13 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -e |
||||||
|
set -u |
||||||
|
set -o pipefail |
||||||
|
|
||||||
|
# Generate LavaMoat policies for the extension background script for each build |
||||||
|
# type. |
||||||
|
# ATTN: This may tax your device when running it locally. |
||||||
|
concurrently --kill-others-on-fail -n main,beta,flask \ |
||||||
|
"WRITE_AUTO_POLICY=1 yarn dist" \ |
||||||
|
"WRITE_AUTO_POLICY=1 yarn dist --build-type beta" \ |
||||||
|
"WRITE_AUTO_POLICY=1 yarn dist --build-type flask" |
@ -0,0 +1,55 @@ |
|||||||
|
{ |
||||||
|
"resources": { |
||||||
|
"browser-resolve": { |
||||||
|
"packages": { |
||||||
|
"core-js": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"babel-runtime": { |
||||||
|
"packages": { |
||||||
|
"@babel/runtime": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"node-fetch": { |
||||||
|
"globals": { |
||||||
|
"fetch": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"lodash": { |
||||||
|
"globals": { |
||||||
|
"setTimeout": true, |
||||||
|
"clearTimeout": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"@ethersproject/random": { |
||||||
|
"globals": { |
||||||
|
"crypto.getRandomValues": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"browser-passworder": { |
||||||
|
"globals": { |
||||||
|
"crypto": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"randombytes": { |
||||||
|
"globals": { |
||||||
|
"crypto.getRandomValues": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"extensionizer": { |
||||||
|
"globals": { |
||||||
|
"console": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"web3": { |
||||||
|
"globals": { |
||||||
|
"XMLHttpRequest": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"storage": { |
||||||
|
"globals": { |
||||||
|
"localStorage": true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,55 @@ |
|||||||
|
{ |
||||||
|
"resources": { |
||||||
|
"browser-resolve": { |
||||||
|
"packages": { |
||||||
|
"core-js": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"babel-runtime": { |
||||||
|
"packages": { |
||||||
|
"@babel/runtime": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"node-fetch": { |
||||||
|
"globals": { |
||||||
|
"fetch": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"lodash": { |
||||||
|
"globals": { |
||||||
|
"setTimeout": true, |
||||||
|
"clearTimeout": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"@ethersproject/random": { |
||||||
|
"globals": { |
||||||
|
"crypto.getRandomValues": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"browser-passworder": { |
||||||
|
"globals": { |
||||||
|
"crypto": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"randombytes": { |
||||||
|
"globals": { |
||||||
|
"crypto.getRandomValues": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"extensionizer": { |
||||||
|
"globals": { |
||||||
|
"console": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"web3": { |
||||||
|
"globals": { |
||||||
|
"XMLHttpRequest": true |
||||||
|
} |
||||||
|
}, |
||||||
|
"storage": { |
||||||
|
"globals": { |
||||||
|
"localStorage": true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,124 @@ |
|||||||
|
import { fireEvent } from '@testing-library/react'; |
||||||
|
import React from 'react'; |
||||||
|
import configureMockStore from 'redux-mock-store'; |
||||||
|
import { renderWithProvider } from '../../../../../test/lib/render-helpers'; |
||||||
|
import { TRANSACTION_ERROR_KEY } from '../../../../helpers/constants/error-keys'; |
||||||
|
import ConfirmPageContainerContent from './confirm-page-container-content.component'; |
||||||
|
|
||||||
|
describe('Confirm Page Container Content', () => { |
||||||
|
const mockStore = { |
||||||
|
metamask: { |
||||||
|
provider: { |
||||||
|
type: 'test', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
const store = configureMockStore()(mockStore); |
||||||
|
|
||||||
|
let props = {}; |
||||||
|
|
||||||
|
beforeEach(() => { |
||||||
|
const mockOnCancel = jest.fn(); |
||||||
|
const mockOnCancelAll = jest.fn(); |
||||||
|
const mockOnSubmit = jest.fn(); |
||||||
|
const mockOnConfirmAnyways = jest.fn(); |
||||||
|
props = { |
||||||
|
action: ' Withdraw Stake', |
||||||
|
errorMessage: null, |
||||||
|
errorKey: null, |
||||||
|
hasSimulationError: true, |
||||||
|
onCancelAll: mockOnCancelAll, |
||||||
|
onCancel: mockOnCancel, |
||||||
|
cancelText: 'Reject', |
||||||
|
onSubmit: mockOnSubmit, |
||||||
|
onConfirmAnyways: mockOnConfirmAnyways, |
||||||
|
submitText: 'Confirm', |
||||||
|
disabled: true, |
||||||
|
origin: 'http://localhost:4200', |
||||||
|
hideTitle: false, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
it('render ConfirmPageContainer component with simulation error', async () => { |
||||||
|
const { queryByText, getByText } = renderWithProvider( |
||||||
|
<ConfirmPageContainerContent {...props} />, |
||||||
|
store, |
||||||
|
); |
||||||
|
|
||||||
|
expect( |
||||||
|
queryByText('Transaction Error. Exception thrown in contract code.'), |
||||||
|
).not.toBeInTheDocument(); |
||||||
|
expect( |
||||||
|
queryByText( |
||||||
|
'This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended.', |
||||||
|
), |
||||||
|
).toBeInTheDocument(); |
||||||
|
expect(queryByText('I will try anyway')).toBeInTheDocument(); |
||||||
|
|
||||||
|
const confirmButton = getByText('Confirm'); |
||||||
|
expect(getByText('Confirm').closest('button')).toBeDisabled(); |
||||||
|
fireEvent.click(confirmButton); |
||||||
|
expect(props.onSubmit).toHaveBeenCalledTimes(0); |
||||||
|
|
||||||
|
const iWillTryButton = getByText('I will try anyway'); |
||||||
|
fireEvent.click(iWillTryButton); |
||||||
|
expect(props.onConfirmAnyways).toHaveBeenCalledTimes(1); |
||||||
|
|
||||||
|
const cancelButton = getByText('Reject'); |
||||||
|
fireEvent.click(cancelButton); |
||||||
|
expect(props.onCancel).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('render ConfirmPageContainer component with another error', async () => { |
||||||
|
props.hasSimulationError = false; |
||||||
|
props.disabled = true; |
||||||
|
props.errorKey = TRANSACTION_ERROR_KEY; |
||||||
|
const { queryByText, getByText } = renderWithProvider( |
||||||
|
<ConfirmPageContainerContent {...props} />, |
||||||
|
store, |
||||||
|
); |
||||||
|
|
||||||
|
expect( |
||||||
|
queryByText( |
||||||
|
'This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended.', |
||||||
|
), |
||||||
|
).not.toBeInTheDocument(); |
||||||
|
expect(queryByText('I will try anyway')).not.toBeInTheDocument(); |
||||||
|
expect(getByText('Confirm').closest('button')).toBeDisabled(); |
||||||
|
expect( |
||||||
|
getByText('Transaction Error. Exception thrown in contract code.'), |
||||||
|
).toBeInTheDocument(); |
||||||
|
|
||||||
|
const cancelButton = getByText('Reject'); |
||||||
|
fireEvent.click(cancelButton); |
||||||
|
expect(props.onCancel).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('render ConfirmPageContainer component with no errors', async () => { |
||||||
|
props.hasSimulationError = false; |
||||||
|
props.disabled = false; |
||||||
|
const { queryByText, getByText } = renderWithProvider( |
||||||
|
<ConfirmPageContainerContent {...props} />, |
||||||
|
store, |
||||||
|
); |
||||||
|
|
||||||
|
expect( |
||||||
|
queryByText( |
||||||
|
'This transaction is expected to fail. Trying to execute it is expected to be expensive but fail, and is not recommended.', |
||||||
|
), |
||||||
|
).not.toBeInTheDocument(); |
||||||
|
expect( |
||||||
|
queryByText('Transaction Error. Exception thrown in contract code.'), |
||||||
|
).not.toBeInTheDocument(); |
||||||
|
expect(queryByText('I will try anyway')).not.toBeInTheDocument(); |
||||||
|
|
||||||
|
const confirmButton = getByText('Confirm'); |
||||||
|
fireEvent.click(confirmButton); |
||||||
|
expect(props.onSubmit).toHaveBeenCalledTimes(1); |
||||||
|
|
||||||
|
const cancelButton = getByText('Reject'); |
||||||
|
fireEvent.click(cancelButton); |
||||||
|
expect(props.onCancel).toHaveBeenCalledTimes(1); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,68 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
|
||||||
|
import { PRIORITY_LEVELS } from '../../../../shared/constants/gas'; |
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||||
|
import Popover from '../../ui/popover'; |
||||||
|
import I18nValue from '../../ui/i18n-value'; |
||||||
|
import LoadingHeartBeat from '../../ui/loading-heartbeat'; |
||||||
|
|
||||||
|
import EditGasItem from './edit-gas-item'; |
||||||
|
|
||||||
|
const EditGasFeePopover = ({ onClose }) => { |
||||||
|
const t = useI18nContext(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Popover |
||||||
|
title={t('editGasFeeModalTitle')} |
||||||
|
onClose={onClose} |
||||||
|
className="edit-gas-fee-popover" |
||||||
|
> |
||||||
|
<> |
||||||
|
{process.env.IN_TEST === 'true' ? null : <LoadingHeartBeat />} |
||||||
|
<div className="edit-gas-fee-popover__wrapper"> |
||||||
|
<div className="edit-gas-fee-popover__content"> |
||||||
|
<div className="edit-gas-fee-popover__content__header"> |
||||||
|
<span className="edit-gas-fee-popover__content__header-option"> |
||||||
|
<I18nValue messageKey="gasOption" /> |
||||||
|
</span> |
||||||
|
<span className="edit-gas-fee-popover__content__header-time"> |
||||||
|
<I18nValue messageKey="time" /> |
||||||
|
</span> |
||||||
|
<span className="edit-gas-fee-popover__content__header-max-fee"> |
||||||
|
<I18nValue messageKey="maxFee" /> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
<EditGasItem |
||||||
|
priorityLevel={PRIORITY_LEVELS.LOW} |
||||||
|
onClose={onClose} |
||||||
|
/> |
||||||
|
<EditGasItem |
||||||
|
priorityLevel={PRIORITY_LEVELS.MEDIUM} |
||||||
|
onClose={onClose} |
||||||
|
/> |
||||||
|
<EditGasItem |
||||||
|
priorityLevel={PRIORITY_LEVELS.HIGH} |
||||||
|
onClose={onClose} |
||||||
|
/> |
||||||
|
<div className="edit-gas-fee-popover__content__separator" /> |
||||||
|
<EditGasItem |
||||||
|
priorityLevel={PRIORITY_LEVELS.DAPP_SUGGESTED} |
||||||
|
onClose={onClose} |
||||||
|
/> |
||||||
|
<EditGasItem |
||||||
|
priorityLevel={PRIORITY_LEVELS.CUSTOM} |
||||||
|
onClose={onClose} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</> |
||||||
|
</Popover> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
EditGasFeePopover.propTypes = { |
||||||
|
onClose: PropTypes.func, |
||||||
|
}; |
||||||
|
|
||||||
|
export default EditGasFeePopover; |
@ -0,0 +1,95 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { screen } from '@testing-library/react'; |
||||||
|
|
||||||
|
import { renderWithProvider } from '../../../../test/lib/render-helpers'; |
||||||
|
import { ETH } from '../../../helpers/constants/common'; |
||||||
|
import configureStore from '../../../store/store'; |
||||||
|
import { GasFeeContextProvider } from '../../../contexts/gasFee'; |
||||||
|
|
||||||
|
import EditGasFeePopover from './edit-gas-fee-popover'; |
||||||
|
|
||||||
|
jest.mock('../../../store/actions', () => ({ |
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(), |
||||||
|
getGasFeeEstimatesAndStartPolling: jest |
||||||
|
.fn() |
||||||
|
.mockImplementation(() => Promise.resolve()), |
||||||
|
addPollingTokenToAppState: jest.fn(), |
||||||
|
})); |
||||||
|
|
||||||
|
const MOCK_FEE_ESTIMATE = { |
||||||
|
low: { |
||||||
|
minWaitTimeEstimate: 360000, |
||||||
|
maxWaitTimeEstimate: 300000, |
||||||
|
suggestedMaxPriorityFeePerGas: '3', |
||||||
|
suggestedMaxFeePerGas: '53', |
||||||
|
}, |
||||||
|
medium: { |
||||||
|
minWaitTimeEstimate: 30000, |
||||||
|
maxWaitTimeEstimate: 60000, |
||||||
|
suggestedMaxPriorityFeePerGas: '7', |
||||||
|
suggestedMaxFeePerGas: '70', |
||||||
|
}, |
||||||
|
high: { |
||||||
|
minWaitTimeEstimate: 15000, |
||||||
|
maxWaitTimeEstimate: 15000, |
||||||
|
suggestedMaxPriorityFeePerGas: '10', |
||||||
|
suggestedMaxFeePerGas: '100', |
||||||
|
}, |
||||||
|
estimatedBaseFee: '50', |
||||||
|
}; |
||||||
|
|
||||||
|
const renderComponent = () => { |
||||||
|
const store = configureStore({ |
||||||
|
metamask: { |
||||||
|
nativeCurrency: ETH, |
||||||
|
provider: {}, |
||||||
|
cachedBalances: {}, |
||||||
|
accounts: { |
||||||
|
'0xAddress': { |
||||||
|
address: '0xAddress', |
||||||
|
balance: '0x176e5b6f173ebe66', |
||||||
|
}, |
||||||
|
}, |
||||||
|
selectedAddress: '0xAddress', |
||||||
|
featureFlags: { advancedInlineGas: true }, |
||||||
|
gasFeeEstimates: MOCK_FEE_ESTIMATE, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
return renderWithProvider( |
||||||
|
<GasFeeContextProvider transaction={{ txParams: { gas: '0x5208' } }}> |
||||||
|
<EditGasFeePopover /> |
||||||
|
</GasFeeContextProvider>, |
||||||
|
store, |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
describe('EditGasFeePopover', () => { |
||||||
|
it('should renders low / medium / high options', () => { |
||||||
|
renderComponent(); |
||||||
|
|
||||||
|
expect(screen.queryByText('🐢')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('🦊')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('🦍')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('🌐')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('⚙')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Low')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Market')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Aggressive')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Site')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Advanced')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should show time estimates', () => { |
||||||
|
renderComponent(); |
||||||
|
expect(screen.queryAllByText('5 min')).toHaveLength(2); |
||||||
|
expect(screen.queryByText('15 sec')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should show gas fee estimates', () => { |
||||||
|
renderComponent(); |
||||||
|
expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,150 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
import classNames from 'classnames'; |
||||||
|
import { useSelector } from 'react-redux'; |
||||||
|
|
||||||
|
import { getMaximumGasTotalInHexWei } from '../../../../../shared/modules/gas.utils'; |
||||||
|
import { PRIORITY_LEVELS } from '../../../../../shared/constants/gas'; |
||||||
|
import { PRIORITY_LEVEL_ICON_MAP } from '../../../../helpers/constants/gas'; |
||||||
|
import { PRIMARY } from '../../../../helpers/constants/common'; |
||||||
|
import { |
||||||
|
decGWEIToHexWEI, |
||||||
|
decimalToHex, |
||||||
|
hexWEIToDecGWEI, |
||||||
|
} from '../../../../helpers/utils/conversions.util'; |
||||||
|
import { getAdvancedGasFeeValues } from '../../../../selectors'; |
||||||
|
import { toHumanReadableTime } from '../../../../helpers/utils/util'; |
||||||
|
import { useGasFeeContext } from '../../../../contexts/gasFee'; |
||||||
|
import { useI18nContext } from '../../../../hooks/useI18nContext'; |
||||||
|
import I18nValue from '../../../ui/i18n-value'; |
||||||
|
import InfoTooltip from '../../../ui/info-tooltip'; |
||||||
|
import UserPreferencedCurrencyDisplay from '../../user-preferenced-currency-display'; |
||||||
|
|
||||||
|
import { useCustomTimeEstimate } from './useCustomTimeEstimate'; |
||||||
|
|
||||||
|
const EditGasItem = ({ priorityLevel, onClose }) => { |
||||||
|
const { |
||||||
|
estimateUsed, |
||||||
|
gasFeeEstimates, |
||||||
|
gasLimit, |
||||||
|
maxFeePerGas: maxFeePerGasValue, |
||||||
|
maxPriorityFeePerGas: maxPriorityFeePerGasValue, |
||||||
|
updateTransactionUsingGasFeeEstimates, |
||||||
|
transaction: { dappSuggestedGasFees }, |
||||||
|
} = useGasFeeContext(); |
||||||
|
const t = useI18nContext(); |
||||||
|
const advancedGasFeeValues = useSelector(getAdvancedGasFeeValues); |
||||||
|
let maxFeePerGas; |
||||||
|
let maxPriorityFeePerGas; |
||||||
|
let minWaitTime; |
||||||
|
|
||||||
|
if (gasFeeEstimates[priorityLevel]) { |
||||||
|
maxFeePerGas = gasFeeEstimates[priorityLevel].suggestedMaxFeePerGas; |
||||||
|
} else if ( |
||||||
|
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED && |
||||||
|
dappSuggestedGasFees |
||||||
|
) { |
||||||
|
maxFeePerGas = hexWEIToDecGWEI(dappSuggestedGasFees.maxFeePerGas); |
||||||
|
maxPriorityFeePerGas = hexWEIToDecGWEI( |
||||||
|
dappSuggestedGasFees.maxPriorityFeePerGas, |
||||||
|
); |
||||||
|
} else if (priorityLevel === PRIORITY_LEVELS.CUSTOM) { |
||||||
|
if (estimateUsed === PRIORITY_LEVELS.CUSTOM) { |
||||||
|
maxFeePerGas = maxFeePerGasValue; |
||||||
|
maxPriorityFeePerGas = maxPriorityFeePerGasValue; |
||||||
|
} else if (advancedGasFeeValues) { |
||||||
|
maxFeePerGas = |
||||||
|
gasFeeEstimates.estimatedBaseFee * |
||||||
|
parseFloat(advancedGasFeeValues.maxBaseFee); |
||||||
|
maxPriorityFeePerGas = advancedGasFeeValues.priorityFee; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const { waitTimeEstimate } = useCustomTimeEstimate({ |
||||||
|
gasFeeEstimates, |
||||||
|
maxFeePerGas, |
||||||
|
maxPriorityFeePerGas, |
||||||
|
}); |
||||||
|
|
||||||
|
if (gasFeeEstimates[priorityLevel]) { |
||||||
|
minWaitTime = |
||||||
|
priorityLevel === PRIORITY_LEVELS.HIGH |
||||||
|
? gasFeeEstimates?.high.minWaitTimeEstimate |
||||||
|
: gasFeeEstimates?.low.maxWaitTimeEstimate; |
||||||
|
} else { |
||||||
|
minWaitTime = waitTimeEstimate; |
||||||
|
} |
||||||
|
|
||||||
|
const hexMaximumTransactionFee = maxFeePerGas |
||||||
|
? getMaximumGasTotalInHexWei({ |
||||||
|
gasLimit: decimalToHex(gasLimit), |
||||||
|
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas), |
||||||
|
}) |
||||||
|
: null; |
||||||
|
|
||||||
|
const onOptionSelect = () => { |
||||||
|
if (priorityLevel !== PRIORITY_LEVELS.CUSTOM) { |
||||||
|
updateTransactionUsingGasFeeEstimates(priorityLevel); |
||||||
|
} |
||||||
|
// todo: open advance modal if priorityLevel is custom
|
||||||
|
onClose(); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className={classNames('edit-gas-item', { |
||||||
|
'edit-gas-item-selected': priorityLevel === estimateUsed, |
||||||
|
'edit-gas-item-disabled': |
||||||
|
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED && |
||||||
|
!dappSuggestedGasFees, |
||||||
|
})} |
||||||
|
role="button" |
||||||
|
onClick={onOptionSelect} |
||||||
|
> |
||||||
|
<span className="edit-gas-item__name"> |
||||||
|
<span |
||||||
|
className={`edit-gas-item__icon edit-gas-item__icon-${priorityLevel}`} |
||||||
|
> |
||||||
|
{PRIORITY_LEVEL_ICON_MAP[priorityLevel]} |
||||||
|
</span> |
||||||
|
<I18nValue |
||||||
|
messageKey={ |
||||||
|
priorityLevel === PRIORITY_LEVELS.DAPP_SUGGESTED |
||||||
|
? 'dappSuggestedShortLabel' |
||||||
|
: priorityLevel |
||||||
|
} |
||||||
|
/> |
||||||
|
</span> |
||||||
|
<span |
||||||
|
className={`edit-gas-item__time-estimate edit-gas-item__time-estimate-${priorityLevel}`} |
||||||
|
> |
||||||
|
{minWaitTime |
||||||
|
? minWaitTime && toHumanReadableTime(t, minWaitTime) |
||||||
|
: '--'} |
||||||
|
</span> |
||||||
|
<span |
||||||
|
className={`edit-gas-item__fee-estimate edit-gas-item__fee-estimate-${priorityLevel}`} |
||||||
|
> |
||||||
|
{hexMaximumTransactionFee ? ( |
||||||
|
<UserPreferencedCurrencyDisplay |
||||||
|
key="editGasSubTextFeeAmount" |
||||||
|
type={PRIMARY} |
||||||
|
value={hexMaximumTransactionFee} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
'--' |
||||||
|
)} |
||||||
|
</span> |
||||||
|
<span className="edit-gas-item__tooltip"> |
||||||
|
<InfoTooltip position="top" /> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
EditGasItem.propTypes = { |
||||||
|
priorityLevel: PropTypes.string, |
||||||
|
onClose: PropTypes.func, |
||||||
|
}; |
||||||
|
|
||||||
|
export default EditGasItem; |
@ -0,0 +1,138 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { screen } from '@testing-library/react'; |
||||||
|
|
||||||
|
import { renderWithProvider } from '../../../../../test/lib/render-helpers'; |
||||||
|
import { ETH } from '../../../../helpers/constants/common'; |
||||||
|
import configureStore from '../../../../store/store'; |
||||||
|
import { GasFeeContextProvider } from '../../../../contexts/gasFee'; |
||||||
|
|
||||||
|
import EditGasItem from './edit-gas-item'; |
||||||
|
|
||||||
|
jest.mock('../../../../store/actions', () => ({ |
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(), |
||||||
|
getGasFeeEstimatesAndStartPolling: jest |
||||||
|
.fn() |
||||||
|
.mockImplementation(() => Promise.resolve()), |
||||||
|
addPollingTokenToAppState: jest.fn(), |
||||||
|
getGasFeeTimeEstimate: jest |
||||||
|
.fn() |
||||||
|
.mockImplementation(() => Promise.resolve('unknown')), |
||||||
|
})); |
||||||
|
|
||||||
|
const MOCK_FEE_ESTIMATE = { |
||||||
|
low: { |
||||||
|
minWaitTimeEstimate: 360000, |
||||||
|
maxWaitTimeEstimate: 300000, |
||||||
|
suggestedMaxPriorityFeePerGas: '3', |
||||||
|
suggestedMaxFeePerGas: '53', |
||||||
|
}, |
||||||
|
medium: { |
||||||
|
minWaitTimeEstimate: 30000, |
||||||
|
maxWaitTimeEstimate: 60000, |
||||||
|
suggestedMaxPriorityFeePerGas: '7', |
||||||
|
suggestedMaxFeePerGas: '70', |
||||||
|
}, |
||||||
|
high: { |
||||||
|
minWaitTimeEstimate: 15000, |
||||||
|
maxWaitTimeEstimate: 15000, |
||||||
|
suggestedMaxPriorityFeePerGas: '10', |
||||||
|
suggestedMaxFeePerGas: '100', |
||||||
|
}, |
||||||
|
estimatedBaseFee: '50', |
||||||
|
}; |
||||||
|
|
||||||
|
const DAPP_SUGGESTED_ESTIMATE = { |
||||||
|
maxFeePerGas: '0x59682f10', |
||||||
|
maxPriorityFeePerGas: '0x59682f00', |
||||||
|
}; |
||||||
|
|
||||||
|
const renderComponent = (props, transactionProps, gasFeeContextProps) => { |
||||||
|
const store = configureStore({ |
||||||
|
metamask: { |
||||||
|
nativeCurrency: ETH, |
||||||
|
provider: {}, |
||||||
|
cachedBalances: {}, |
||||||
|
accounts: { |
||||||
|
'0xAddress': { |
||||||
|
address: '0xAddress', |
||||||
|
balance: '0x176e5b6f173ebe66', |
||||||
|
}, |
||||||
|
}, |
||||||
|
selectedAddress: '0xAddress', |
||||||
|
featureFlags: { advancedInlineGas: true }, |
||||||
|
gasFeeEstimates: MOCK_FEE_ESTIMATE, |
||||||
|
advancedGasFee: { |
||||||
|
maxBaseFee: '1.5', |
||||||
|
priorityFee: '2', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
return renderWithProvider( |
||||||
|
<GasFeeContextProvider |
||||||
|
transaction={{ txParams: { gas: '0x5208' }, ...transactionProps }} |
||||||
|
{...gasFeeContextProps} |
||||||
|
> |
||||||
|
<EditGasItem priorityLevel="low" {...props} /> |
||||||
|
</GasFeeContextProvider>, |
||||||
|
store, |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
describe('EditGasItem', () => { |
||||||
|
it('should renders low gas estimate option for priorityLevel low', () => { |
||||||
|
renderComponent({ priorityLevel: 'low' }); |
||||||
|
expect(screen.queryByText('🐢')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Low')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('5 min')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByTitle('0.001113 ETH')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should renders market gas estimate option for priorityLevel medium', () => { |
||||||
|
renderComponent({ priorityLevel: 'medium' }); |
||||||
|
expect(screen.queryByText('🦊')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Market')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('5 min')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByTitle('0.00147 ETH')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should renders aggressive gas estimate option for priorityLevel high', () => { |
||||||
|
renderComponent({ priorityLevel: 'high' }); |
||||||
|
expect(screen.queryByText('🦍')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Aggressive')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('15 sec')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByTitle('0.0021 ETH')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should highlight option is priorityLevel is currently selected', () => { |
||||||
|
renderComponent({ priorityLevel: 'high' }, { userFeeLevel: 'high' }); |
||||||
|
expect( |
||||||
|
document.getElementsByClassName('edit-gas-item-selected'), |
||||||
|
).toHaveLength(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should renders site gas estimate option for priorityLevel dappSuggested', () => { |
||||||
|
renderComponent( |
||||||
|
{ priorityLevel: 'dappSuggested' }, |
||||||
|
{ dappSuggestedGasFees: DAPP_SUGGESTED_ESTIMATE }, |
||||||
|
); |
||||||
|
expect(screen.queryByText('🌐')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Site')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByTitle('0.0000315 ETH')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should disable site gas estimate option for is transaction does not have dappSuggestedGasFees', async () => { |
||||||
|
renderComponent({ priorityLevel: 'dappSuggested' }); |
||||||
|
expect( |
||||||
|
document.getElementsByClassName('edit-gas-item-disabled'), |
||||||
|
).toHaveLength(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should renders advance gas estimate option for priorityLevel custom', () => { |
||||||
|
renderComponent({ priorityLevel: 'custom' }); |
||||||
|
expect(screen.queryByText('⚙')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Advanced')).toBeInTheDocument(); |
||||||
|
// below value of custom gas fee estimate is default obtained from state.metamask.advancedGasFee
|
||||||
|
expect(screen.queryByTitle('0.001575 ETH')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './edit-gas-item'; |
@ -0,0 +1,68 @@ |
|||||||
|
.edit-gas-item { |
||||||
|
border-radius: 24px; |
||||||
|
color: $ui-4; |
||||||
|
cursor: pointer; |
||||||
|
font-size: 12px; |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
margin: 12px 0; |
||||||
|
padding: 4px 12px; |
||||||
|
height: 32px; |
||||||
|
|
||||||
|
&--selected { |
||||||
|
background-color: $ui-1; |
||||||
|
} |
||||||
|
|
||||||
|
&-disabled { |
||||||
|
cursor: default; |
||||||
|
} |
||||||
|
|
||||||
|
&__name { |
||||||
|
display: inline-flex; |
||||||
|
align-items: center; |
||||||
|
color: $ui-black; |
||||||
|
font-size: 12px; |
||||||
|
font-weight: bold; |
||||||
|
width: 40%; |
||||||
|
} |
||||||
|
|
||||||
|
&__icon { |
||||||
|
margin-right: 4px; |
||||||
|
|
||||||
|
&-custom { |
||||||
|
font-size: 20px; |
||||||
|
line-height: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__time-estimate { |
||||||
|
display: inline-block; |
||||||
|
width: 20%; |
||||||
|
} |
||||||
|
|
||||||
|
&__fee-estimate { |
||||||
|
display: inline-block; |
||||||
|
width: 30%; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
|
||||||
|
&__tooltip { |
||||||
|
display: inline-block; |
||||||
|
text-align: right; |
||||||
|
width: 10%; |
||||||
|
|
||||||
|
.info-tooltip { |
||||||
|
display: inline-block; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__time-estimate-low, |
||||||
|
&__fee-estimate-high { |
||||||
|
color: $secondary-1; |
||||||
|
} |
||||||
|
|
||||||
|
&__time-estimate-medium, |
||||||
|
&__time-estimate-high { |
||||||
|
color: $success-3; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,83 @@ |
|||||||
|
import { useEffect, useState } from 'react'; |
||||||
|
import { useSelector } from 'react-redux'; |
||||||
|
import BigNumber from 'bignumber.js'; |
||||||
|
|
||||||
|
import { GAS_ESTIMATE_TYPES } from '../../../../../shared/constants/gas'; |
||||||
|
import { |
||||||
|
getGasEstimateType, |
||||||
|
getIsGasEstimatesLoading, |
||||||
|
} from '../../../../ducks/metamask/metamask'; |
||||||
|
import { getGasFeeTimeEstimate } from '../../../../store/actions'; |
||||||
|
|
||||||
|
export const useCustomTimeEstimate = ({ |
||||||
|
gasFeeEstimates, |
||||||
|
maxFeePerGas, |
||||||
|
maxPriorityFeePerGas, |
||||||
|
}) => { |
||||||
|
const gasEstimateType = useSelector(getGasEstimateType); |
||||||
|
const isGasEstimatesLoading = useSelector(getIsGasEstimatesLoading); |
||||||
|
|
||||||
|
const [customEstimatedTime, setCustomEstimatedTime] = useState(null); |
||||||
|
|
||||||
|
const returnNoEstimates = |
||||||
|
isGasEstimatesLoading || |
||||||
|
gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET || |
||||||
|
!maxPriorityFeePerGas; |
||||||
|
|
||||||
|
// If the user has chosen a value lower than the low gas fee estimate,
|
||||||
|
// We'll need to use the useEffect hook below to make a call to calculate
|
||||||
|
// the time to show
|
||||||
|
const isUnknownLow = |
||||||
|
gasFeeEstimates?.low && |
||||||
|
Number(maxPriorityFeePerGas) < |
||||||
|
Number(gasFeeEstimates.low.suggestedMaxPriorityFeePerGas); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if ( |
||||||
|
isGasEstimatesLoading || |
||||||
|
gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET || |
||||||
|
!maxPriorityFeePerGas |
||||||
|
) |
||||||
|
return; |
||||||
|
if (isUnknownLow) { |
||||||
|
// getGasFeeTimeEstimate requires parameters in string format
|
||||||
|
getGasFeeTimeEstimate( |
||||||
|
new BigNumber(maxPriorityFeePerGas, 10).toString(10), |
||||||
|
new BigNumber(maxFeePerGas, 10).toString(10), |
||||||
|
).then((result) => { |
||||||
|
setCustomEstimatedTime(result); |
||||||
|
}); |
||||||
|
} |
||||||
|
}, [ |
||||||
|
gasEstimateType, |
||||||
|
isUnknownLow, |
||||||
|
isGasEstimatesLoading, |
||||||
|
maxFeePerGas, |
||||||
|
maxPriorityFeePerGas, |
||||||
|
returnNoEstimates, |
||||||
|
]); |
||||||
|
|
||||||
|
if (returnNoEstimates) { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
const { low = {}, medium = {}, high = {} } = gasFeeEstimates; |
||||||
|
let waitTimeEstimate = ''; |
||||||
|
|
||||||
|
if ( |
||||||
|
isUnknownLow && |
||||||
|
customEstimatedTime && |
||||||
|
customEstimatedTime !== 'unknown' && |
||||||
|
customEstimatedTime?.upperTimeBound !== 'unknown' |
||||||
|
) { |
||||||
|
waitTimeEstimate = Number(customEstimatedTime?.upperTimeBound); |
||||||
|
} else if ( |
||||||
|
Number(maxPriorityFeePerGas) >= Number(medium.suggestedMaxPriorityFeePerGas) |
||||||
|
) { |
||||||
|
waitTimeEstimate = high.minWaitTimeEstimate; |
||||||
|
} else { |
||||||
|
waitTimeEstimate = low.maxWaitTimeEstimate; |
||||||
|
} |
||||||
|
|
||||||
|
return { waitTimeEstimate }; |
||||||
|
}; |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './edit-gas-fee-popover'; |
@ -0,0 +1,40 @@ |
|||||||
|
.edit-gas-fee-popover { |
||||||
|
@media screen and (min-width: $break-large) { |
||||||
|
max-height: 84vh; |
||||||
|
} |
||||||
|
|
||||||
|
&__wrapper { |
||||||
|
border-top: 1px solid $ui-grey; |
||||||
|
} |
||||||
|
|
||||||
|
&__content { |
||||||
|
padding: 16px 12px; |
||||||
|
|
||||||
|
&__header { |
||||||
|
color: $ui-4; |
||||||
|
font-size: 10px; |
||||||
|
font-weight: 700; |
||||||
|
margin: 0 12px; |
||||||
|
|
||||||
|
&-option { |
||||||
|
display: inline-block; |
||||||
|
width: 40%; |
||||||
|
} |
||||||
|
|
||||||
|
&-time { |
||||||
|
display: inline-block; |
||||||
|
width: 20%; |
||||||
|
} |
||||||
|
|
||||||
|
&-max-fee { |
||||||
|
display: inline-block; |
||||||
|
width: 30%; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&__separator { |
||||||
|
border-top: 1px solid $ui-grey; |
||||||
|
margin: 8px 12px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
export { default } from './new-collectibles-notice.component'; |
@ -0,0 +1,56 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import Box from '../../ui/box'; |
||||||
|
import Dialog from '../../ui/dialog'; |
||||||
|
import Typography from '../../ui/typography/typography'; |
||||||
|
import { |
||||||
|
COLORS, |
||||||
|
TYPOGRAPHY, |
||||||
|
TEXT_ALIGN, |
||||||
|
FONT_WEIGHT, |
||||||
|
DISPLAY, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||||
|
|
||||||
|
export default function NewCollectiblesNotice() { |
||||||
|
const t = useI18nContext(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box marginBottom={8}> |
||||||
|
<Dialog type="message"> |
||||||
|
<Box display={DISPLAY.FLEX}> |
||||||
|
<Box paddingTop={2}> |
||||||
|
<i style={{ fontSize: '1rem' }} className="fa fa-info-circle" /> |
||||||
|
</Box> |
||||||
|
<Box paddingLeft={4}> |
||||||
|
<Typography |
||||||
|
color={COLORS.BLACK} |
||||||
|
align={TEXT_ALIGN.LEFT} |
||||||
|
variant={TYPOGRAPHY.Paragraph} |
||||||
|
fontWeight={FONT_WEIGHT.BOLD} |
||||||
|
> |
||||||
|
{t('newNFTsDetected')} |
||||||
|
</Typography> |
||||||
|
<Typography |
||||||
|
color={COLORS.BLACK} |
||||||
|
align={TEXT_ALIGN.LEFT} |
||||||
|
variant={TYPOGRAPHY.Paragraph} |
||||||
|
boxProps={{ marginBottom: 4 }} |
||||||
|
> |
||||||
|
{t('newNFTsDetectedInfo')} |
||||||
|
</Typography> |
||||||
|
<a |
||||||
|
href="#" |
||||||
|
onClick={(e) => { |
||||||
|
e.preventDefault(); |
||||||
|
console.log('show preference popover'); |
||||||
|
}} |
||||||
|
style={{ fontSize: '.9rem' }} |
||||||
|
> |
||||||
|
{t('selectNFTPrivacyPreference')} |
||||||
|
</a> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</Dialog> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,94 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { screen } from '@testing-library/react'; |
||||||
|
|
||||||
|
import { ETH } from '../../../helpers/constants/common'; |
||||||
|
import { GasFeeContextProvider } from '../../../contexts/gasFee'; |
||||||
|
import { renderWithProvider } from '../../../../test/jest'; |
||||||
|
import configureStore from '../../../store/store'; |
||||||
|
|
||||||
|
import TransactionDetail from './transaction-detail.component'; |
||||||
|
|
||||||
|
jest.mock('../../../store/actions', () => ({ |
||||||
|
disconnectGasFeeEstimatePoller: jest.fn(), |
||||||
|
getGasFeeEstimatesAndStartPolling: jest |
||||||
|
.fn() |
||||||
|
.mockImplementation(() => Promise.resolve()), |
||||||
|
addPollingTokenToAppState: jest.fn(), |
||||||
|
})); |
||||||
|
|
||||||
|
const render = (props) => { |
||||||
|
const store = configureStore({ |
||||||
|
metamask: { |
||||||
|
nativeCurrency: ETH, |
||||||
|
preferences: { |
||||||
|
useNativeCurrencyAsPrimaryCurrency: true, |
||||||
|
}, |
||||||
|
provider: {}, |
||||||
|
cachedBalances: {}, |
||||||
|
accounts: { |
||||||
|
'0xAddress': { |
||||||
|
address: '0xAddress', |
||||||
|
balance: '0x176e5b6f173ebe66', |
||||||
|
}, |
||||||
|
}, |
||||||
|
selectedAddress: '0xAddress', |
||||||
|
featureFlags: { advancedInlineGas: true }, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
return renderWithProvider( |
||||||
|
<GasFeeContextProvider {...props}> |
||||||
|
<TransactionDetail |
||||||
|
onEdit={() => { |
||||||
|
console.log('on edit'); |
||||||
|
}} |
||||||
|
rows={[]} |
||||||
|
{...props} |
||||||
|
/> |
||||||
|
</GasFeeContextProvider>, |
||||||
|
store, |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
describe('TransactionDetail', () => { |
||||||
|
beforeEach(() => { |
||||||
|
process.env.EIP_1559_V2 = true; |
||||||
|
}); |
||||||
|
afterEach(() => { |
||||||
|
process.env.EIP_1559_V2 = false; |
||||||
|
}); |
||||||
|
it('should render edit link with text low if low gas estimates are selected', () => { |
||||||
|
render({ transaction: { userFeeLevel: 'low' } }); |
||||||
|
expect(screen.queryByText('🐢')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Low')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
it('should render edit link with text markey if medium gas estimates are selected', () => { |
||||||
|
render({ transaction: { userFeeLevel: 'medium' } }); |
||||||
|
expect(screen.queryByText('🦊')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Market')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
it('should render edit link with text agressive if high gas estimates are selected', () => { |
||||||
|
render({ transaction: { userFeeLevel: 'high' } }); |
||||||
|
expect(screen.queryByText('🦍')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Aggressive')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
it('should render edit link with text Site suggested if site suggested estimated are used', () => { |
||||||
|
render({ |
||||||
|
transaction: { |
||||||
|
dappSuggestedGasFees: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 }, |
||||||
|
txParams: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 }, |
||||||
|
}, |
||||||
|
}); |
||||||
|
expect(screen.queryByText('🌐')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Site suggested')).toBeInTheDocument(); |
||||||
|
expect(document.getElementsByClassName('info-tooltip')).toHaveLength(1); |
||||||
|
}); |
||||||
|
it('should render edit link with text advance if custom gas estimates are used', () => { |
||||||
|
render({ |
||||||
|
defaultEstimateToUse: 'custom', |
||||||
|
}); |
||||||
|
expect(screen.queryByText('⚙')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Advanced')).toBeInTheDocument(); |
||||||
|
expect(screen.queryByText('Edit')).toBeInTheDocument(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,42 @@ |
|||||||
|
import { Story, Canvas, ArgsTable } from '@storybook/addon-docs'; |
||||||
|
|
||||||
|
import Card from '.'; |
||||||
|
|
||||||
|
# Card |
||||||
|
|
||||||
|
Cards are used to group related content or actions together. |
||||||
|
|
||||||
|
<Canvas> |
||||||
|
<Story id="ui-components-ui-card-card-stories-js--default-story" /> |
||||||
|
</Canvas> |
||||||
|
|
||||||
|
## Component API |
||||||
|
|
||||||
|
The `Card` component extends the `Box` component. See the `Box` component for an extended list of props. |
||||||
|
|
||||||
|
<ArgsTable of={Card} /> |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
The following describes the props and example usage for this component. |
||||||
|
|
||||||
|
### Padding, Border and Background Color |
||||||
|
|
||||||
|
The Card component has a set of default props that should meet most card use cases. There is a strong recommendation to not overwrite these to ensure our cards stay consistent across the app. |
||||||
|
|
||||||
|
That being said all props can be overwritten if necessary. |
||||||
|
|
||||||
|
```jsx |
||||||
|
import { COLORS } from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
// To remove the border |
||||||
|
<Card border={false} /> |
||||||
|
// All border related props of the Box component will work |
||||||
|
|
||||||
|
// To remove or change padding |
||||||
|
<Card padding={0} /> |
||||||
|
// All padding related props of the Box component will work |
||||||
|
|
||||||
|
// To change the background color |
||||||
|
<Card backgroundColor={COLORS.UI4} /> |
||||||
|
``` |
@ -1,23 +0,0 @@ |
|||||||
import React, { PureComponent } from 'react'; |
|
||||||
import PropTypes from 'prop-types'; |
|
||||||
import classnames from 'classnames'; |
|
||||||
|
|
||||||
export default class Card extends PureComponent { |
|
||||||
static propTypes = { |
|
||||||
className: PropTypes.string, |
|
||||||
overrideClassName: PropTypes.bool, |
|
||||||
title: PropTypes.string, |
|
||||||
children: PropTypes.node, |
|
||||||
}; |
|
||||||
|
|
||||||
render() { |
|
||||||
const { className, overrideClassName, title } = this.props; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className={classnames({ card: !overrideClassName }, className)}> |
|
||||||
<div className="card__title">{title}</div> |
|
||||||
{this.props.children} |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { shallow } from 'enzyme'; |
|
||||||
import Card from './card.component'; |
|
||||||
|
|
||||||
describe('Card Component', () => { |
|
||||||
it('should render a card with a title and child element', () => { |
|
||||||
const wrapper = shallow( |
|
||||||
<Card title="Test" className="card-test-class"> |
|
||||||
<div className="child-test-class">Child</div> |
|
||||||
</Card>, |
|
||||||
); |
|
||||||
|
|
||||||
expect(wrapper.hasClass('card-test-class')).toStrictEqual(true); |
|
||||||
const title = wrapper.find('.card__title'); |
|
||||||
expect(title).toHaveLength(1); |
|
||||||
expect(title.text()).toStrictEqual('Test'); |
|
||||||
const child = wrapper.find('.child-test-class'); |
|
||||||
expect(child).toHaveLength(1); |
|
||||||
expect(child.text()).toStrictEqual('Child'); |
|
||||||
}); |
|
||||||
}); |
|
@ -0,0 +1,60 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import PropTypes from 'prop-types'; |
||||||
|
|
||||||
|
import Box from '../box'; |
||||||
|
import { |
||||||
|
BORDER_STYLE, |
||||||
|
COLORS, |
||||||
|
SIZES, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
const Card = ({ |
||||||
|
border = true, |
||||||
|
padding = 4, |
||||||
|
backgroundColor = COLORS.WHITE, |
||||||
|
children, |
||||||
|
...props |
||||||
|
}) => { |
||||||
|
const defaultBorderProps = { |
||||||
|
borderColor: border && COLORS.UI2, |
||||||
|
borderRadius: border && SIZES.MD, |
||||||
|
borderStyle: border && BORDER_STYLE.SOLID, |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
{...{ |
||||||
|
padding, |
||||||
|
backgroundColor, |
||||||
|
...defaultBorderProps, |
||||||
|
...props, |
||||||
|
}} |
||||||
|
> |
||||||
|
{children} |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
Card.propTypes = { |
||||||
|
/** |
||||||
|
* Whether the Card has a border or not. |
||||||
|
* Defaults to true |
||||||
|
*/ |
||||||
|
border: PropTypes.bool, |
||||||
|
/** |
||||||
|
* Padding of the Card component accepts number or an array of 2 numbers. |
||||||
|
* Defaults to 4 (16px) |
||||||
|
*/ |
||||||
|
padding: Box.propTypes.padding, |
||||||
|
/** |
||||||
|
* The background color of the card |
||||||
|
* Defaults to COLORS.WHITE |
||||||
|
*/ |
||||||
|
backgroundColor: Box.propTypes.backgroundColor, |
||||||
|
/** |
||||||
|
* The Card component accepts all Box component props |
||||||
|
*/ |
||||||
|
...Box.propTypes, |
||||||
|
}; |
||||||
|
|
||||||
|
export default Card; |
@ -0,0 +1,169 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import { |
||||||
|
ALIGN_ITEMS, |
||||||
|
BLOCK_SIZES, |
||||||
|
BORDER_STYLE, |
||||||
|
COLORS, |
||||||
|
DISPLAY, |
||||||
|
JUSTIFY_CONTENT, |
||||||
|
TEXT_ALIGN, |
||||||
|
} from '../../../helpers/constants/design-system'; |
||||||
|
|
||||||
|
import README from './README.mdx'; |
||||||
|
import Card from '.'; |
||||||
|
|
||||||
|
const sizeOptions = [undefined, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; |
||||||
|
|
||||||
|
export default { |
||||||
|
title: 'UI/Card', |
||||||
|
id: __filename, |
||||||
|
component: Card, |
||||||
|
parameters: { |
||||||
|
docs: { |
||||||
|
page: README, |
||||||
|
}, |
||||||
|
}, |
||||||
|
argTypes: { |
||||||
|
children: { control: 'text' }, |
||||||
|
border: { |
||||||
|
control: 'boolean', |
||||||
|
}, |
||||||
|
borderStyle: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(BORDER_STYLE), |
||||||
|
}, |
||||||
|
borderWidth: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
borderColor: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(COLORS), |
||||||
|
}, |
||||||
|
backgroundColor: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(COLORS), |
||||||
|
}, |
||||||
|
width: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(BLOCK_SIZES), |
||||||
|
}, |
||||||
|
height: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(BLOCK_SIZES), |
||||||
|
}, |
||||||
|
textAlign: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(TEXT_ALIGN), |
||||||
|
}, |
||||||
|
margin: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
marginTop: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
marginRight: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
marginBottom: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
marginLeft: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
padding: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
paddingTop: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
paddingRight: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
paddingBottom: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
paddingLeft: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: [...sizeOptions], |
||||||
|
}, |
||||||
|
display: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(DISPLAY), |
||||||
|
}, |
||||||
|
justifyContent: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(JUSTIFY_CONTENT), |
||||||
|
}, |
||||||
|
alignItems: { |
||||||
|
control: { |
||||||
|
type: 'select', |
||||||
|
}, |
||||||
|
options: Object.values(ALIGN_ITEMS), |
||||||
|
}, |
||||||
|
}, |
||||||
|
args: { |
||||||
|
children: 'Card children', |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
export const DefaultStory = (args) => <Card {...args}>{args.children}</Card>; |
||||||
|
|
||||||
|
DefaultStory.storyName = 'Default'; |
||||||
|
|
||||||
|
DefaultStory.args = { |
||||||
|
padding: 4, |
||||||
|
border: true, |
||||||
|
borderWidth: 1, |
||||||
|
borderColor: COLORS.UI2, |
||||||
|
borderStyle: BORDER_STYLE.SOLID, |
||||||
|
backgroundColor: COLORS.WHITE, |
||||||
|
display: DISPLAY.BLOCK, |
||||||
|
}; |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue