mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Restore timing function (#8774)
* restore and enhance the time est feature background: we had a feature for showing a time estimate on pending txs that was accidently removed during the redesign implementation. This PR restores that feature and also enhances it: 1. Displays the time estimate on all views instead of just fullscreen 2. Uses Intl.RelativeTimeFormat to format the time 3. Adds a way to toggle the feature flag. 4. Uses a hook to calculate the time remaining instead of a component * Update app/_locales/en/messages.json Co-authored-by: Mark Stacey <markjstacey@gmail.com> * do not display on test nets Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
parent
5aabe2ac75
commit
2f50e9fd72
@ -1603,6 +1603,9 @@
|
||||
"transactionTime": {
|
||||
"message": "Transaction Time"
|
||||
},
|
||||
"showTransactionTimeDescription": {
|
||||
"message": "Select this to display pending transaction time estimates in the activity tab while on the Main Ethereum Network. Note: estimates are approximations based on network conditions."
|
||||
},
|
||||
"transfer": {
|
||||
"message": "Transfer"
|
||||
},
|
||||
|
@ -4,6 +4,7 @@ import './lib/freezeGlobals'
|
||||
|
||||
// polyfills
|
||||
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch'
|
||||
import '@formatjs/intl-relativetimeformat/polyfill'
|
||||
|
||||
import PortStream from 'extension-port-stream'
|
||||
import { getEnvironmentType } from './lib/util'
|
||||
|
@ -71,6 +71,7 @@
|
||||
"3box": "^1.10.2",
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@download/blockies": "^1.0.3",
|
||||
"@formatjs/intl-relativetimeformat": "^5.2.6",
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"@material-ui/core": "1.0.0",
|
||||
"@metamask/controllers": "^2.0.0",
|
||||
|
@ -92,6 +92,8 @@
|
||||
|
||||
@import '../ui/icon/index';
|
||||
|
||||
@import '../ui/icon-with-label/index';
|
||||
|
||||
@import '../ui/circle-icon/index';
|
||||
|
||||
@import '../ui/alert-circle-icon/index';
|
||||
|
@ -22,6 +22,8 @@ import {
|
||||
import { useShouldShowSpeedUp } from '../../../hooks/useShouldShowSpeedUp'
|
||||
import TransactionStatus from '../transaction-status/transaction-status.component'
|
||||
import TransactionIcon from '../transaction-icon'
|
||||
import { useTransactionTimeRemaining } from '../../../hooks/useTransactionTimeRemaining'
|
||||
import IconWithLabel from '../../ui/icon-with-label'
|
||||
|
||||
|
||||
export default function TransactionListItem ({ transactionGroup, isEarliestNonce = false }) {
|
||||
@ -30,8 +32,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
const { hasCancelled } = transactionGroup
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
|
||||
const { initialTransaction: { id }, primaryTransaction } = transactionGroup
|
||||
|
||||
const { initialTransaction: { id }, primaryTransaction: { err, submittedTime, gasPrice } } = transactionGroup
|
||||
const [cancelEnabled, cancelTransaction] = useCancelTransaction(transactionGroup)
|
||||
const retryTransaction = useRetryTransaction(transactionGroup)
|
||||
const shouldShowSpeedUp = useShouldShowSpeedUp(transactionGroup, isEarliestNonce)
|
||||
@ -49,6 +50,9 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
senderAddress,
|
||||
} = useTransactionDisplayData(transactionGroup)
|
||||
|
||||
const timeRemaining = useTransactionTimeRemaining(isPending, isEarliestNonce, submittedTime, gasPrice)
|
||||
|
||||
|
||||
const isSignatureReq = category === TRANSACTION_CATEGORY_SIGNATURE_REQUEST
|
||||
const isUnapproved = status === UNAPPROVED_STATUS
|
||||
|
||||
@ -112,9 +116,9 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
className={className}
|
||||
title={title}
|
||||
titleIcon={!isUnapproved && isPending && isEarliestNonce && (
|
||||
<Preloader
|
||||
size={16}
|
||||
color="#D73A49"
|
||||
<IconWithLabel
|
||||
icon={<Preloader size={16} color="#D73A49" />}
|
||||
label={timeRemaining}
|
||||
/>
|
||||
)}
|
||||
icon={<TransactionIcon category={category} status={status} />}
|
||||
@ -123,7 +127,7 @@ export default function TransactionListItem ({ transactionGroup, isEarliestNonce
|
||||
<TransactionStatus
|
||||
isPending={isPending}
|
||||
isEarliestNonce={isEarliestNonce}
|
||||
error={primaryTransaction.err}
|
||||
error={err}
|
||||
date={date}
|
||||
status={status}
|
||||
/>
|
||||
|
@ -1 +0,0 @@
|
||||
export { default } from './transaction-time-remaining.container'
|
@ -1,52 +0,0 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { calcTransactionTimeRemaining } from './transaction-time-remaining.util'
|
||||
|
||||
export default class TransactionTimeRemaining extends PureComponent {
|
||||
static propTypes = {
|
||||
className: PropTypes.string,
|
||||
initialTimeEstimate: PropTypes.number,
|
||||
submittedTime: PropTypes.number,
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
const { initialTimeEstimate, submittedTime } = props
|
||||
this.state = {
|
||||
timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime),
|
||||
}
|
||||
this.interval = setInterval(
|
||||
() => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }),
|
||||
1000
|
||||
)
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
const { initialTimeEstimate, submittedTime } = this.props
|
||||
if (initialTimeEstimate !== prevProps.initialTimeEstimate) {
|
||||
clearInterval(this.interval)
|
||||
const calcedTimeRemaining = calcTransactionTimeRemaining(initialTimeEstimate, submittedTime)
|
||||
this.setState({ timeRemaining: calcedTimeRemaining })
|
||||
this.interval = setInterval(
|
||||
() => this.setState({ timeRemaining: calcTransactionTimeRemaining(initialTimeEstimate, submittedTime) }),
|
||||
1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.interval)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className } = this.props
|
||||
const { timeRemaining } = this.state
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{ timeRemaining }
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
import { connect } from 'react-redux'
|
||||
import TransactionTimeRemaining from './transaction-time-remaining.component'
|
||||
import {
|
||||
getEstimatedGasPrices,
|
||||
getEstimatedGasTimes,
|
||||
} from '../../../selectors'
|
||||
import { getRawTimeEstimateData } from '../../../helpers/utils/gas-time-estimates.util'
|
||||
import { hexWEIToDecGWEI } from '../../../helpers/utils/conversions.util'
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { transaction } = ownProps
|
||||
const { gasPrice: currentGasPrice } = transaction.txParams
|
||||
const customGasPrice = calcCustomGasPrice(currentGasPrice)
|
||||
const gasPrices = getEstimatedGasPrices(state)
|
||||
const estimatedTimes = getEstimatedGasTimes(state)
|
||||
|
||||
const {
|
||||
newTimeEstimate: initialTimeEstimate,
|
||||
} = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes)
|
||||
|
||||
const submittedTime = transaction.submittedTime
|
||||
|
||||
return {
|
||||
initialTimeEstimate,
|
||||
submittedTime,
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(TransactionTimeRemaining)
|
||||
|
||||
function calcCustomGasPrice (customGasPriceInHex) {
|
||||
return Number(hexWEIToDecGWEI(customGasPriceInHex))
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { formatTimeEstimate } from '../../../helpers/utils/gas-time-estimates.util'
|
||||
|
||||
export function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
|
||||
const currentTime = (new Date()).getTime()
|
||||
const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000
|
||||
const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission
|
||||
|
||||
const renderingTimeRemainingEstimate = timeRemainingOnEstimate < 30
|
||||
? '< 30 s'
|
||||
: formatTimeEstimate(timeRemainingOnEstimate)
|
||||
|
||||
return renderingTimeRemainingEstimate
|
||||
}
|
18
ui/app/components/ui/icon-with-label/icon-with-label.js
Normal file
18
ui/app/components/ui/icon-with-label/icon-with-label.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export default function IconWithLabel ({ icon, label, className }) {
|
||||
return (
|
||||
<div className={classnames('icon-with-label', className)}>
|
||||
{icon}
|
||||
{label && <span className="icon-with-label__label">{label}</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
IconWithLabel.propTypes = {
|
||||
icon: PropTypes.node.isRequired,
|
||||
className: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
}
|
1
ui/app/components/ui/icon-with-label/index.js
Normal file
1
ui/app/components/ui/icon-with-label/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './icon-with-label'
|
10
ui/app/components/ui/icon-with-label/index.scss
Normal file
10
ui/app/components/ui/icon-with-label/index.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.icon-with-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
margin-left: 4px;
|
||||
color: $Grey-500;
|
||||
}
|
||||
}
|
@ -33,12 +33,11 @@
|
||||
font-size: 16px;
|
||||
line-height: 160%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-wrap {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
@ -26,9 +26,9 @@ export default function ListItem ({
|
||||
)}
|
||||
<h2 className="list-item__heading">
|
||||
{ title } {titleIcon && (
|
||||
<span className="list-item__heading-wrap">
|
||||
<div className="list-item__heading-wrap">
|
||||
{titleIcon}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</h2>
|
||||
<h3 className="list-item__subheading">
|
||||
|
98
ui/app/hooks/useTransactionTimeRemaining.js
Normal file
98
ui/app/hooks/useTransactionTimeRemaining.js
Normal file
@ -0,0 +1,98 @@
|
||||
import { getEstimatedGasPrices, getEstimatedGasTimes, getFeatureFlags, getIsMainnet } from '../selectors'
|
||||
import { hexWEIToDecGWEI } from '../helpers/utils/conversions.util'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useRef, useEffect, useState, useMemo } from 'react'
|
||||
import { isEqual } from 'lodash'
|
||||
import { getRawTimeEstimateData } from '../helpers/utils/gas-time-estimates.util'
|
||||
import { getCurrentLocale } from '../ducks/metamask/metamask'
|
||||
|
||||
|
||||
/**
|
||||
* Calculate the number of minutes remaining until the transaction completes.
|
||||
* @param {number} initialTimeEstimate - timestamp for the projected completion time
|
||||
* @param {number} submittedTime - timestamp of when the tx was submitted
|
||||
* @return {number} minutes remaining
|
||||
*/
|
||||
function calcTransactionTimeRemaining (initialTimeEstimate, submittedTime) {
|
||||
const currentTime = (new Date()).getTime()
|
||||
const timeElapsedSinceSubmission = (currentTime - submittedTime) / 1000
|
||||
const timeRemainingOnEstimate = initialTimeEstimate - timeElapsedSinceSubmission
|
||||
|
||||
const renderingTimeRemainingEstimate = Math.round(timeRemainingOnEstimate / 60)
|
||||
return renderingTimeRemainingEstimate
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a string representing the number of minutes predicted for the transaction to be
|
||||
* completed. Only returns this prediction if the transaction is the earliest pending
|
||||
* transaction, and the feature flag for showing timing is enabled.
|
||||
* @param {bool} isPending - is the transaction currently pending
|
||||
* @param {bool} isEarliestNonce - is this transaction the earliest nonce in list
|
||||
* @param {number} submittedTime - the timestamp for when the transaction was submitted
|
||||
* @param {number} currentGasPrice - gas price to use for calculation of time
|
||||
* @returns {string | undefined} i18n formatted string if applicable
|
||||
*/
|
||||
export function useTransactionTimeRemaining (
|
||||
isPending,
|
||||
isEarliestNonce,
|
||||
submittedTime,
|
||||
currentGasPrice
|
||||
) {
|
||||
// the following two selectors return the result of mapping over an array, as such they
|
||||
// will always be new objects and trigger effects. To avoid this, we use isEqual as the
|
||||
// equalityFn to only update when the data is new.
|
||||
const gasPrices = useSelector(getEstimatedGasPrices, isEqual)
|
||||
const estimatedTimes = useSelector(getEstimatedGasTimes, isEqual)
|
||||
const locale = useSelector(getCurrentLocale)
|
||||
const isMainNet = useSelector(getIsMainnet)
|
||||
const interval = useRef()
|
||||
const [timeRemaining, setTimeRemaining] = useState(null)
|
||||
const featureFlags = useSelector(getFeatureFlags)
|
||||
const transactionTimeFeatureActive = featureFlags?.transactionTime
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'narrow' })
|
||||
|
||||
// Memoize this value so it can be used as a dependency in the effect below
|
||||
const initialTimeEstimate = useMemo(() => {
|
||||
const customGasPrice = Number(hexWEIToDecGWEI(currentGasPrice))
|
||||
const {
|
||||
newTimeEstimate,
|
||||
} = getRawTimeEstimateData(customGasPrice, gasPrices, estimatedTimes)
|
||||
return newTimeEstimate
|
||||
}, [ currentGasPrice, gasPrices, estimatedTimes ])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isMainNet &&
|
||||
transactionTimeFeatureActive &&
|
||||
isPending &&
|
||||
isEarliestNonce &&
|
||||
!isNaN(initialTimeEstimate)
|
||||
) {
|
||||
clearInterval(interval.current)
|
||||
setTimeRemaining(
|
||||
calcTransactionTimeRemaining(initialTimeEstimate, submittedTime)
|
||||
)
|
||||
interval.current = setInterval(() => {
|
||||
setTimeRemaining(
|
||||
calcTransactionTimeRemaining(initialTimeEstimate, submittedTime)
|
||||
)
|
||||
}, 10000)
|
||||
return () => clearInterval(interval.current)
|
||||
}
|
||||
}, [
|
||||
isMainNet,
|
||||
transactionTimeFeatureActive,
|
||||
isEarliestNonce,
|
||||
isPending,
|
||||
submittedTime,
|
||||
initialTimeEstimate,
|
||||
])
|
||||
|
||||
// there are numerous checks to determine if time should be displayed.
|
||||
// if any of the following are true, the timeRemaining will be null
|
||||
// User is currently not on the mainnet
|
||||
// User does not have the transactionTime feature flag enabled
|
||||
// The transaction is not pending, or isn't the earliest nonce
|
||||
return timeRemaining ? rtf.format(timeRemaining, 'minute') : undefined
|
||||
}
|
@ -24,6 +24,8 @@ export default class AdvancedTab extends PureComponent {
|
||||
sendHexData: PropTypes.bool,
|
||||
setAdvancedInlineGasFeatureFlag: PropTypes.func,
|
||||
advancedInlineGas: PropTypes.bool,
|
||||
setTransactionTimeFeatureFlag: PropTypes.func,
|
||||
transactionTime: PropTypes.bool,
|
||||
showFiatInTestnets: PropTypes.bool,
|
||||
autoLockTimeLimit: PropTypes.number,
|
||||
setAutoLockTimeLimit: PropTypes.func.isRequired,
|
||||
@ -194,6 +196,32 @@ export default class AdvancedTab extends PureComponent {
|
||||
)
|
||||
}
|
||||
|
||||
renderTransactionTimeEstimates () {
|
||||
const { t } = this.context
|
||||
const { transactionTime, setTransactionTimeFeatureFlag } = this.props
|
||||
|
||||
return (
|
||||
<div className="settings-page__content-row" data-testid="advanced-setting-transaction-time-inline">
|
||||
<div className="settings-page__content-item">
|
||||
<span>{ t('transactionTime') }</span>
|
||||
<div className="settings-page__content-description">
|
||||
{ t('showTransactionTimeDescription') }
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-page__content-item">
|
||||
<div className="settings-page__content-item-col">
|
||||
<ToggleButton
|
||||
value={transactionTime}
|
||||
onToggle={(value) => setTransactionTimeFeatureFlag(!value)}
|
||||
offLabel={t('off')}
|
||||
onLabel={t('on')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderShowConversionInTestnets () {
|
||||
const { t } = this.context
|
||||
const {
|
||||
@ -447,6 +475,7 @@ export default class AdvancedTab extends PureComponent {
|
||||
{ this.renderMobileSync() }
|
||||
{ this.renderResetAccount() }
|
||||
{ this.renderAdvancedGasInputInline() }
|
||||
{ this.renderTransactionTimeEstimates() }
|
||||
{ this.renderHexDataOptIn() }
|
||||
{ this.renderShowConversionInTestnets() }
|
||||
{ this.renderUseNonceOptIn() }
|
||||
|
@ -20,6 +20,7 @@ export const mapStateToProps = (state) => {
|
||||
const {
|
||||
featureFlags: {
|
||||
sendHexData,
|
||||
transactionTime,
|
||||
advancedInlineGas,
|
||||
} = {},
|
||||
threeBoxSyncingAllowed,
|
||||
@ -33,6 +34,7 @@ export const mapStateToProps = (state) => {
|
||||
warning,
|
||||
sendHexData,
|
||||
advancedInlineGas,
|
||||
transactionTime,
|
||||
showFiatInTestnets,
|
||||
autoLockTimeLimit,
|
||||
threeBoxSyncingAllowed,
|
||||
@ -48,6 +50,7 @@ export const mapDispatchToProps = (dispatch) => {
|
||||
displayWarning: (warning) => dispatch(displayWarning(warning)),
|
||||
showResetAccountConfirmationModal: () => dispatch(showModal({ name: 'CONFIRM_RESET_ACCOUNT' })),
|
||||
setAdvancedInlineGasFeatureFlag: (shouldShow) => dispatch(setFeatureFlag('advancedInlineGas', shouldShow)),
|
||||
setTransactionTimeFeatureFlag: (shouldShow) => dispatch(setFeatureFlag('transactionTime', shouldShow)),
|
||||
setUseNonceField: (value) => dispatch(setUseNonceField(value)),
|
||||
setShowFiatConversionOnTestnetsPreference: (value) => {
|
||||
return dispatch(setShowFiatConversionOnTestnetsPreference(value))
|
||||
|
@ -24,7 +24,7 @@ describe('AdvancedTab Component', function () {
|
||||
}
|
||||
)
|
||||
|
||||
assert.equal(root.find('.settings-page__content-row').length, 10)
|
||||
assert.equal(root.find('.settings-page__content-row').length, 11)
|
||||
})
|
||||
|
||||
it('should update autoLockTimeLimit', function () {
|
||||
@ -46,7 +46,7 @@ describe('AdvancedTab Component', function () {
|
||||
}
|
||||
)
|
||||
|
||||
const autoTimeout = root.find('.settings-page__content-row').at(7)
|
||||
const autoTimeout = root.find('.settings-page__content-row').at(8)
|
||||
const textField = autoTimeout.find(TextField)
|
||||
|
||||
textField.props().onChange({ target: { value: 1440 } })
|
||||
|
19
yarn.lock
19
yarn.lock
@ -1261,6 +1261,20 @@
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
|
||||
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
|
||||
|
||||
"@formatjs/intl-relativetimeformat@^5.2.6":
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-5.2.6.tgz#3d67b75a900e7b5416615beeb2d0eeff33a1e01a"
|
||||
integrity sha512-UPCY7IoyeqieUxdbfhINVjbCGXCzRr4xZpoiNsr1da4Fwm4uV6l53OXsx1zDRXoiNmMtDuKCKkRzlSfBL89L1g==
|
||||
dependencies:
|
||||
"@formatjs/intl-utils" "^3.3.1"
|
||||
|
||||
"@formatjs/intl-utils@^3.3.1":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/intl-utils/-/intl-utils-3.3.1.tgz#7ceadbb7e251318729d9bf693731e1a5dcdfa15a"
|
||||
integrity sha512-7AAicg2wqCJQ+gFEw5Nxp+ttavajBrPAD1HDmzA4jzvUCrF5a2NCJm/c5qON3VBubWWF2cu8HglEouj2h/l7KQ==
|
||||
dependencies:
|
||||
emojis-list "^3.0.0"
|
||||
|
||||
"@fortawesome/fontawesome-free@^5.13.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.13.0.tgz#fcb113d1aca4b471b709e8c9c168674fbd6e06d9"
|
||||
@ -9162,6 +9176,11 @@ emojis-list@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k=
|
||||
|
||||
emojis-list@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
|
||||
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
|
||||
|
||||
emotion-theming@^10.0.19:
|
||||
version "10.0.27"
|
||||
resolved "https://registry.yarnpkg.com/emotion-theming/-/emotion-theming-10.0.27.tgz#1887baaec15199862c89b1b984b79806f2b9ab10"
|
||||
|
Loading…
Reference in New Issue
Block a user