Skip to content

Commit

Permalink
Onboarding: add time remaining and progress ring
Browse files Browse the repository at this point in the history
Fixes #29

* Adds a loading state for transactions by subscribing to the
'transactionHash' web3 event.
* Consumes EthGasStation API to obtain an estimate of the
confirmation delay for each transaction.
* Shows the time remaining when the transaction is broadcasted
* Created a new Progress Ring component to show the progress using the
remaining time estimation

**Note**: this PR was not tested for templates with multiple transactions.
  • Loading branch information
e18r committed Dec 17, 2019
1 parent af3d38b commit 994b25c
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 57 deletions.
54 changes: 39 additions & 15 deletions src/onboarding/Create/Create.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ function useDeploymentState(
signing: 0,
error: -1,
})
const [submittedTransactions, setSubmittedTransactions] = useState({})

const deployTransactions = useMemo(
() =>
Expand All @@ -247,7 +248,7 @@ function useDeploymentState(
// Call tx functions in the template, one after another.
useEffect(() => {
if (attempts === 0) {
setTransactionProgress({ signed: 0, errored: -1 })
setTransactionProgress({ submitted: -1, signed: -1, errored: -1 })
} else {
setTransactionProgress(txProgress => ({ ...txProgress, errored: -1 }))
}
Expand All @@ -262,7 +263,7 @@ function useDeploymentState(
deployTransactions
// If we're retrying, only retry from the last signed one
.slice(transactionProgress.signed)
.reduce(async (deployPromise, { transaction }) => {
.reduce(async (deployPromise, { transaction }, i) => {
// Wait for the previous promise; if component has unmounted, don't progress any further
await deployPromise

Expand All @@ -276,20 +277,37 @@ function useDeploymentState(

if (!cancelled) {
try {
await walletWeb3.eth.sendTransaction(transaction)
await walletWeb3.eth
.sendTransaction(transaction)
.on('transactionHash', async hash => {
setTransactionProgress(prev => ({
...prev,
submitted: prev.submitted + 1,
}))
const submittedTransaction = await walletWeb3.eth.getTransaction(
hash
)
setSubmittedTransactions(prev => ({
...prev,
[i]: {
dateStart: new Date(),
gasPrice: submittedTransaction.gasPrice / 1000000000,
},
}))
})

if (!cancelled) {
setTransactionProgress(({ signed, errored }) => ({
signed: signed + 1,
errored,
setTransactionProgress(prev => ({
...prev,
signed: prev.signed + 1,
}))
}
} catch (err) {
log('Failed onboarding transaction', err)
if (!cancelled) {
setTransactionProgress(({ signed, errored }) => ({
errored: signed,
signed,
setTransactionProgress(prev => ({
...prev,
errored: prev.signed,
}))
}

Expand All @@ -312,25 +330,31 @@ function useDeploymentState(
return []
}

const { signed, errored } = transactionProgress
const { submitted, signed, errored } = transactionProgress
const status = index => {
if (errored !== -1 && index >= errored) {
return TRANSACTION_STATUS_ERROR
}
if (index === signed) {
return TRANSACTION_STATUS_PENDING
}
if (index < signed) {
if (index <= signed) {
return TRANSACTION_STATUS_SUCCESS
}
if (index <= submitted) {
return TRANSACTION_STATUS_PENDING
}
return TRANSACTION_STATUS_UPCOMING
}

return deployTransactions.map(({ name }, index) => ({
name,
status: status(index),
dateStart: submittedTransactions[index]
? submittedTransactions[index].dateStart
: undefined,
gasPrice: submittedTransactions[index]
? submittedTransactions[index].gasPrice
: undefined,
}))
}, [deployTransactions, transactionProgress])
}, [deployTransactions, transactionProgress, submittedTransactions])

return {
deployTransactions,
Expand Down
1 change: 0 additions & 1 deletion src/onboarding/Deployment/Deployment.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@ const Deployment = React.memo(function Deployment({
ready,
transactionsStatus,
}) {
// TODO: handle loading state for transactions
const { above } = useViewport()

// TODO: handle transaction error
Expand Down
224 changes: 191 additions & 33 deletions src/onboarding/Deployment/DeploymentStepsItem.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import { textStyle, GU, IconCheck, useTheme } from '@aragon/ui'
import { TransactionStatusType } from '../../prop-types'
Expand All @@ -7,9 +7,61 @@ import {
TRANSACTION_STATUS_SUCCESS,
TRANSACTION_STATUS_UPCOMING,
} from '../../symbols'
import styled from 'styled-components'
import { addMinutes } from 'date-fns'

function DeploymentStepsItem({ index, name, status }) {
function DeploymentStepsItem({ index, name, status, dateStart, gasPrice }) {
const theme = useTheme()
const [remainingTime, setRemainingTime] = useState(0)
const [remainingTimeUnit, setRemainingTimeUnit] = useState('')
const [completedFraction, setCompletedFraction] = useState(0)

const updateTime = useCallback(
dateEnd => {
const now = new Date()
if (now > dateEnd) {
setRemainingTime(0)
setRemainingTimeUnit('sec')
setCompletedFraction(1)
return
}
const time = (dateEnd - now) / 1000
const unit = time < 60 ? 'sec' : time < 3600 ? 'min' : 'hr'
const amount = Math.floor(
unit === 'sec' ? time : unit === 'min' ? 1 + time / 60 : 1 + time / 3600
)
const fraction = (now - dateStart) / (dateEnd - dateStart)
setRemainingTime(amount)
setRemainingTimeUnit(unit)
setCompletedFraction(fraction)
},
[dateStart]
)

useMemo(() => {
if (gasPrice === undefined) return
fetch('https:ethgasstation.info/json/predictTable.json')
.then(response =>
response
.json()
.then(data => {
const index = data.findIndex(d => d.gasprice >= gasPrice)
const priceAbove = data[index]
const priceBelow = data[index > 0 ? index - 1 : index]
const priceCloser =
priceAbove - gasPrice < gasPrice - priceBelow
? priceAbove
: priceBelow
const expectedTime = priceCloser.expectedTime
const dateEnd = addMinutes(dateStart, expectedTime)
updateTime(dateEnd)
setInterval(() => updateTime(dateEnd), 1000)
return null
})
.catch(console.err)
)
.catch(console.err)
}, [dateStart, gasPrice, updateTime])

const stepStyles = useMemo(() => {
if (status === TRANSACTION_STATUS_PENDING) {
Expand Down Expand Up @@ -39,27 +91,31 @@ function DeploymentStepsItem({ index, name, status }) {
margin-top: ${3 * GU}px;
`}
>
<div
css={`
width: ${5 * GU}px;
height: ${5 * GU}px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 18px;
font-weight: 600;
${stepStyles};
flex-shrink: 0;
flex-grow: 0;
`}
>
{status === TRANSACTION_STATUS_SUCCESS ? (
<IconCheck />
) : (
status === TRANSACTION_STATUS_UPCOMING && index + 1
)}
</div>
{status === TRANSACTION_STATUS_PENDING ? (
<ProgressRing value={completedFraction} diameter={5 * GU} />
) : (
<div
css={`
width: ${5 * GU}px;
height: ${5 * GU}px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
font-size: 18px;
font-weight: 600;
${stepStyles};
flex-shrink: 0;
flex-grow: 0;
`}
>
{status === TRANSACTION_STATUS_SUCCESS ? (
<IconCheck />
) : (
status === TRANSACTION_STATUS_UPCOMING && index + 1
)}
</div>
)}
<div
css={`
margin-left: ${3 * GU}px;
Expand All @@ -70,16 +126,18 @@ function DeploymentStepsItem({ index, name, status }) {
`}
>
<div>{name}</div>
{status === TRANSACTION_STATUS_SUCCESS && (
<div
css={`
${textStyle('body3')};
color: ${theme.surfaceContentSecondary};
`}
>
Signature successful
</div>
)}
<div
css={`
${textStyle('body3')};
color: ${theme.surfaceContentSecondary};
`}
>
<StatusMessage
status={status}
remainingTime={remainingTime}
remainingTimeUnit={remainingTimeUnit}
/>
</div>
</div>
</div>
)
Expand All @@ -89,6 +147,106 @@ DeploymentStepsItem.propTypes = {
index: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
status: TransactionStatusType.isRequired,
dateStart: PropTypes.objectOf(Date),
gasPrice: PropTypes.number,
}

const StatusMessage = ({ status, remainingTime, remainingTimeUnit }) => {
switch (status) {
case TRANSACTION_STATUS_UPCOMING:
return 'Waiting for signature'
case TRANSACTION_STATUS_PENDING:
const displayTime = ` - ${remainingTime} ${remainingTimeUnit}`
return `Transaction in progress${displayTime}`
case TRANSACTION_STATUS_SUCCESS:
return ''
}
}

StatusMessage.propTypes = {
status: TransactionStatusType.isRequired,
remainingTime: PropTypes.number,
remainingTimeUnit: PropTypes.string,
}

const ProgressRing = ({ value, diameter }) => {
const theme = useTheme()
return (
<RingContainer diameter={diameter}>
<RingBorder diameter={diameter} theme={theme} />
<Half side="right" value={value} diameter={diameter} />
<Half side="left" value={value} diameter={diameter} />
</RingContainer>
)
}

ProgressRing.propTypes = {
value: PropTypes.number.isRequired,
diameter: PropTypes.number.isRequired,
}

const Half = ({ side, value, diameter }) => {
const theme = useTheme()
let angle
if (side === 'right') {
if (value <= 0.5) {
angle = value * 360
} else {
angle = 180
}
} else {
if (value > 0.5) {
angle = value * 360
} else {
angle = 180
}
}
return (
<HalfHider side={side} diameter={diameter}>
<HalfRing angle={angle} diameter={diameter} theme={theme} />
</HalfHider>
)
}

Half.propTypes = {
side: PropTypes.oneOf(['right', 'left']),
value: PropTypes.number.isRequired,
diameter: PropTypes.number.isRequired,
}

const RingContainer = styled.div`
position: relative;
width: ${({ diameter }) => diameter}px;
`

const RingBorder = styled.div`
height: ${({ diameter }) => diameter}px;
border: 2px solid ${({ theme }) => theme.border};
border-radius: 50%;
`

const HalfHider = styled.div`
position: absolute;
top: 0px;
width: ${({ diameter }) => diameter}px;
height: ${({ diameter }) => diameter}px;
clip-path: inset(
${({ side, diameter }) =>
side === 'right'
? `0px 0px 0px ${diameter / 2}px`
: `0px ${diameter / 2}px 0px 0px`}
);
`

const HalfRing = styled.div`
position: absolute;
top: 0px;
width: ${({ diameter }) => diameter}px;
height: ${({ diameter }) => diameter}px;
border: 2px solid ${({ theme }) => theme.selected};
border-radius: 50%;
clip-path: inset(0px ${({ diameter }) => diameter / 2}px 0px 0px);
transform: rotate(${({ angle }) => angle}deg);
`

export default DeploymentStepsItem
Loading

0 comments on commit 994b25c

Please sign in to comment.