1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02: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:
Brad Decker 2020-06-12 13:46:01 -05:00 committed by GitHub
parent 5aabe2ac75
commit 2f50e9fd72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 201 additions and 112 deletions

View File

@ -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"
},

View File

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

View File

@ -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",

View File

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

View File

@ -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}
/>

View File

@ -1 +0,0 @@
export { default } from './transaction-time-remaining.container'

View File

@ -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>
)
}
}

View File

@ -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))
}

View File

@ -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
}

View 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,
}

View File

@ -0,0 +1 @@
export { default } from './icon-with-label'

View File

@ -0,0 +1,10 @@
.icon-with-label {
display: flex;
align-items: center;
&__label {
font-size: 10px;
margin-left: 4px;
color: $Grey-500;
}
}

View File

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

View File

@ -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">

View 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
}

View File

@ -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() }

View File

@ -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))

View File

@ -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 } })

View File

@ -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"