Merge pull request #4691 from MetaMask/i4404-confirm-refactor
Refactor and redesign confirm transaction viewsfeature/default_network_editable
commit
0d4dbbec2a
After Width: | Height: | Size: 774 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 992 B |
File diff suppressed because one or more lines are too long
@ -1,8 +1,33 @@ |
||||
<html> |
||||
<head> |
||||
<title>E2E Test Dapp</title> |
||||
</head> |
||||
<body> |
||||
<button id="deployButton">Deploy Contract</button> |
||||
<button id="depositButton">Deposit</button> |
||||
<button id="withdrawButton">Withdraw</button> |
||||
</body> |
||||
<div style="display: flex; flex-flow: column;"> |
||||
<div style="display: flex; font-size: 1.25rem;">Contract</div> |
||||
<div style="display: flex;"> |
||||
<button id="deployButton">Deploy Contract</button> |
||||
<button id="depositButton">Deposit</button> |
||||
<button id="withdrawButton">Withdraw</button> |
||||
</div> |
||||
</div> |
||||
<div style="display: flex; flex-flow: column;"> |
||||
<div style="display: flex; font-size: 1.25rem;">Send eth</div> |
||||
<div style="display: flex;"> |
||||
<button id="sendButton">Send</button> |
||||
</div> |
||||
</div> |
||||
<div style="display: flex; flex-flow: column;"> |
||||
<div style="display: flex; font-size: 1.25rem;">Send tokens</div> |
||||
<div id="tokenAddress"></div> |
||||
<div style="display: flex;"> |
||||
<button id="createToken">Create Token</button> |
||||
<button id="transferTokens">Transfer Tokens</button> |
||||
<button id="approveTokens">Approve Tokens</button> |
||||
</div> |
||||
</div> |
||||
|
||||
<script src="contract.js"></script> |
||||
</body> |
||||
|
||||
</html> |
@ -0,0 +1,52 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
|
||||
const ConfirmDetailRow = props => { |
||||
const { |
||||
label, |
||||
fiatFee, |
||||
ethFee, |
||||
onHeaderClick, |
||||
fiatFeeColor, |
||||
headerText, |
||||
headerTextClassName, |
||||
} = props |
||||
|
||||
return ( |
||||
<div className="confirm-detail-row"> |
||||
<div className="confirm-detail-row__label"> |
||||
{ label } |
||||
</div> |
||||
<div className="confirm-detail-row__details"> |
||||
<div |
||||
className={classnames('confirm-detail-row__header-text', headerTextClassName)} |
||||
onClick={() => onHeaderClick && onHeaderClick()} |
||||
> |
||||
{ headerText } |
||||
</div> |
||||
<div |
||||
className="confirm-detail-row__fiat" |
||||
style={{ color: fiatFeeColor }} |
||||
> |
||||
{ fiatFee } |
||||
</div> |
||||
<div className="confirm-detail-row__eth"> |
||||
{ `\u2666 ${ethFee}` } |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
ConfirmDetailRow.propTypes = { |
||||
label: PropTypes.string, |
||||
fiatFee: PropTypes.string, |
||||
ethFee: PropTypes.string, |
||||
fiatFeeColor: PropTypes.string, |
||||
onHeaderClick: PropTypes.func, |
||||
headerText: PropTypes.string, |
||||
headerTextClassName: PropTypes.string, |
||||
} |
||||
|
||||
export default ConfirmDetailRow |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-detail-row.component' |
@ -0,0 +1,43 @@ |
||||
.confirm-detail-row { |
||||
padding: 14px 0; |
||||
display: flex; |
||||
flex-direction: row; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
|
||||
&__label { |
||||
font-size: .75rem; |
||||
font-weight: 500; |
||||
color: $scorpion; |
||||
text-transform: uppercase; |
||||
} |
||||
|
||||
&__details { |
||||
flex: 1; |
||||
text-align: end; |
||||
} |
||||
|
||||
&__fiat { |
||||
font-size: 1.5rem; |
||||
} |
||||
|
||||
&__eth { |
||||
color: $oslo-gray; |
||||
} |
||||
|
||||
&__header-text { |
||||
font-size: .75rem; |
||||
text-transform: uppercase; |
||||
margin-bottom: 6px; |
||||
color: $scorpion; |
||||
|
||||
&--edit { |
||||
color: $curious-blue; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&--total { |
||||
font-size: .625rem; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,105 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import { Tabs, Tab } from '../../tabs' |
||||
import { |
||||
ConfirmPageContainerSummary, |
||||
ConfirmPageContainerError, |
||||
ConfirmPageContainerWarning, |
||||
} from './' |
||||
|
||||
export default class ConfirmPageContainerContent extends Component { |
||||
static propTypes = { |
||||
action: PropTypes.string, |
||||
dataComponent: PropTypes.node, |
||||
detailsComponent: PropTypes.node, |
||||
errorKey: PropTypes.string, |
||||
errorMessage: PropTypes.string, |
||||
hideSubtitle: PropTypes.bool, |
||||
identiconAddress: PropTypes.string, |
||||
nonce: PropTypes.string, |
||||
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
summaryComponent: PropTypes.node, |
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
titleComponent: PropTypes.func, |
||||
warning: PropTypes.string, |
||||
} |
||||
|
||||
renderContent () { |
||||
const { detailsComponent, dataComponent } = this.props |
||||
|
||||
if (detailsComponent && dataComponent) { |
||||
return this.renderTabs() |
||||
} else { |
||||
return detailsComponent || dataComponent |
||||
} |
||||
} |
||||
|
||||
renderTabs () { |
||||
const { detailsComponent, dataComponent } = this.props |
||||
|
||||
return ( |
||||
<Tabs> |
||||
<Tab name="Details"> |
||||
{ detailsComponent } |
||||
</Tab> |
||||
<Tab name="Data"> |
||||
{ dataComponent } |
||||
</Tab> |
||||
</Tabs> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
action, |
||||
errorKey, |
||||
errorMessage, |
||||
title, |
||||
subtitle, |
||||
hideSubtitle, |
||||
identiconAddress, |
||||
nonce, |
||||
summaryComponent, |
||||
detailsComponent, |
||||
dataComponent, |
||||
warning, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<div className="confirm-page-container-content"> |
||||
{ |
||||
warning && ( |
||||
<ConfirmPageContainerWarning warning={warning} /> |
||||
) |
||||
} |
||||
{ |
||||
summaryComponent || ( |
||||
<ConfirmPageContainerSummary |
||||
className={classnames({ |
||||
'confirm-page-container-summary--border': !detailsComponent || !dataComponent, |
||||
})} |
||||
action={action} |
||||
title={title} |
||||
subtitle={subtitle} |
||||
hideSubtitle={hideSubtitle} |
||||
identiconAddress={identiconAddress} |
||||
nonce={nonce} |
||||
/> |
||||
) |
||||
} |
||||
{ this.renderContent() } |
||||
{ |
||||
(errorKey || errorMessage) && ( |
||||
<div className="confirm-page-container-content__error-container"> |
||||
<ConfirmPageContainerError |
||||
errorMessage={errorMessage} |
||||
errorKey={errorKey} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
const ConfirmPageContainerError = (props, context) => { |
||||
const { errorMessage, errorKey } = props |
||||
const error = errorKey ? context.t(errorKey) : errorMessage |
||||
|
||||
return ( |
||||
<div className="confirm-page-container-error"> |
||||
<img |
||||
src="/images/alert-red.svg" |
||||
className="confirm-page-container-error__icon" |
||||
/> |
||||
{ `ALERT: ${error}` } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
ConfirmPageContainerError.propTypes = { |
||||
errorMessage: PropTypes.string, |
||||
errorKey: PropTypes.string, |
||||
} |
||||
|
||||
ConfirmPageContainerError.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
export default ConfirmPageContainerError |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-page-container-error.component' |
@ -0,0 +1,17 @@ |
||||
.confirm-page-container-error { |
||||
height: 32px; |
||||
border: 1px solid $monzo; |
||||
color: $monzo; |
||||
background: lighten($monzo, 56%); |
||||
border-radius: 4px; |
||||
font-size: .75rem; |
||||
display: flex; |
||||
justify-content: flex-start; |
||||
align-items: center; |
||||
padding-left: 16px; |
||||
|
||||
&__icon { |
||||
margin-right: 8px; |
||||
flex: 0 0 auto; |
||||
} |
||||
} |
@ -0,0 +1,56 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import Identicon from '../../../identicon' |
||||
|
||||
const ConfirmPageContainerSummary = props => { |
||||
const { action, title, subtitle, hideSubtitle, className, identiconAddress, nonce } = props |
||||
|
||||
return ( |
||||
<div className={classnames('confirm-page-container-summary', className)}> |
||||
<div className="confirm-page-container-summary__action-row"> |
||||
<div className="confirm-page-container-summary__action"> |
||||
{ action } |
||||
</div> |
||||
{ |
||||
nonce && ( |
||||
<div className="confirm-page-container-summary__nonce"> |
||||
{ `#${nonce}` } |
||||
</div> |
||||
) |
||||
} |
||||
</div> |
||||
<div className="confirm-page-container-summary__title"> |
||||
{ |
||||
identiconAddress && ( |
||||
<Identicon |
||||
className="confirm-page-container-summary__identicon" |
||||
diameter={36} |
||||
address={identiconAddress} |
||||
/> |
||||
) |
||||
} |
||||
<div className="confirm-page-container-summary__title-text"> |
||||
{ title } |
||||
</div> |
||||
</div> |
||||
{ |
||||
hideSubtitle || <div className="confirm-page-container-summary__subtitle"> |
||||
{ subtitle } |
||||
</div> |
||||
} |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
ConfirmPageContainerSummary.propTypes = { |
||||
action: PropTypes.string, |
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
||||
hideSubtitle: PropTypes.bool, |
||||
className: PropTypes.string, |
||||
identiconAddress: PropTypes.string, |
||||
nonce: PropTypes.string, |
||||
} |
||||
|
||||
export default ConfirmPageContainerSummary |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-page-container-summary.component' |
@ -0,0 +1,54 @@ |
||||
.confirm-page-container-summary { |
||||
padding: 16px 24px 0; |
||||
background-color: #f9fafa; |
||||
height: 133px; |
||||
box-sizing: border-box; |
||||
|
||||
&__action-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
&__action { |
||||
text-transform: uppercase; |
||||
color: $oslo-gray; |
||||
font-size: .75rem; |
||||
padding: 3px 8px; |
||||
border: 1px solid $oslo-gray; |
||||
border-radius: 4px; |
||||
display: inline-block; |
||||
} |
||||
|
||||
&__nonce { |
||||
color: $oslo-gray; |
||||
} |
||||
|
||||
&__title { |
||||
padding: 4px 0; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__identicon { |
||||
flex: 0 0 auto; |
||||
margin-right: 8px; |
||||
} |
||||
|
||||
&__title-text { |
||||
font-size: 2.25rem; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
&__subtitle { |
||||
color: $oslo-gray; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
&--border { |
||||
border-bottom: 1px solid $geyser; |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
import React from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
const ConfirmPageContainerWarning = props => { |
||||
return ( |
||||
<div className="confirm-page-container-warning"> |
||||
<img |
||||
className="confirm-page-container-warning__icon" |
||||
src="/images/alert.svg" |
||||
/> |
||||
<div className="confirm-page-container-warning__warning"> |
||||
{ props.warning } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
ConfirmPageContainerWarning.propTypes = { |
||||
warning: PropTypes.string, |
||||
} |
||||
|
||||
export default ConfirmPageContainerWarning |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-page-container-warning.component' |
@ -0,0 +1,18 @@ |
||||
.confirm-page-container-warning { |
||||
background-color: #fffcdb; |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
border-bottom: 1px solid $geyser; |
||||
padding: 12px 24px; |
||||
|
||||
&__icon { |
||||
flex: 0 0 auto; |
||||
margin-right: 16px; |
||||
} |
||||
|
||||
&__warning { |
||||
font-size: .75rem; |
||||
color: #5f5922; |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
export { default } from './confirm-page-container-content.component' |
||||
export { default as ConfirmPageContainerSummary } from './confirm-page-container-summary' |
||||
export { default as ConfirmPageContainerError } from './confirm-page-container-error' |
||||
export { default as ConfirmPageContainerWarning } from './confirm-page-container-warning' |
@ -0,0 +1,66 @@ |
||||
@import './confirm-page-container-error/index'; |
||||
|
||||
@import './confirm-page-container-warning/index'; |
||||
|
||||
@import './confirm-page-container-summary/index'; |
||||
|
||||
.confirm-page-container-content { |
||||
overflow-y: auto; |
||||
flex: 1; |
||||
|
||||
&__error-container { |
||||
padding: 0 16px 16px 16px; |
||||
} |
||||
|
||||
&__details { |
||||
box-sizing: border-box; |
||||
padding: 0 24px; |
||||
} |
||||
|
||||
&__data { |
||||
padding: 16px; |
||||
color: $oslo-gray; |
||||
} |
||||
|
||||
&__data-box { |
||||
background-color: #f9fafa; |
||||
padding: 12px; |
||||
font-size: .75rem; |
||||
margin-bottom: 16px; |
||||
word-wrap: break-word; |
||||
max-height: 200px; |
||||
overflow-y: auto; |
||||
|
||||
&-label { |
||||
text-transform: uppercase; |
||||
padding: 8px 0 12px; |
||||
font-size: 12px; |
||||
} |
||||
} |
||||
|
||||
&__data-field { |
||||
display: flex; |
||||
flex-direction: row; |
||||
|
||||
&-label { |
||||
font-weight: 500; |
||||
padding-right: 16px; |
||||
} |
||||
|
||||
&:not(:last-child) { |
||||
margin-bottom: 5px; |
||||
} |
||||
} |
||||
|
||||
&__gas-fee { |
||||
border-bottom: 1px solid $geyser; |
||||
} |
||||
|
||||
&__function-type { |
||||
font-size: .875rem; |
||||
font-weight: 500; |
||||
text-transform: capitalize; |
||||
color: $black; |
||||
padding-left: 5px; |
||||
} |
||||
} |
@ -0,0 +1,63 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { |
||||
ENVIRONMENT_TYPE_POPUP, |
||||
ENVIRONMENT_TYPE_NOTIFICATION, |
||||
} from '../../../../../app/scripts/lib/enums' |
||||
import NetworkDisplay from '../../network-display' |
||||
|
||||
export default class ConfirmPageContainer extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
showEdit: PropTypes.bool, |
||||
onEdit: PropTypes.func, |
||||
children: PropTypes.node, |
||||
} |
||||
|
||||
renderTop () { |
||||
const { onEdit, showEdit } = this.props |
||||
const windowType = window.METAMASK_UI_TYPE |
||||
const isFullScreen = windowType !== ENVIRONMENT_TYPE_NOTIFICATION && |
||||
windowType !== ENVIRONMENT_TYPE_POPUP |
||||
|
||||
if (!showEdit && isFullScreen) { |
||||
return null |
||||
} |
||||
|
||||
return ( |
||||
<div className="confirm-page-container-header__row"> |
||||
<div |
||||
className="confirm-page-container-header__back-button-container" |
||||
style={{ |
||||
visibility: showEdit ? 'initial' : 'hidden', |
||||
}} |
||||
> |
||||
<img |
||||
src="/images/caret-left.svg" |
||||
/> |
||||
<span |
||||
className="confirm-page-container-header__back-button" |
||||
onClick={() => onEdit()} |
||||
> |
||||
{ this.context.t('edit') } |
||||
</span> |
||||
</div> |
||||
{ !isFullScreen && <NetworkDisplay /> } |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { children } = this.props |
||||
|
||||
return ( |
||||
<div className="confirm-page-container-header"> |
||||
{ this.renderTop() } |
||||
{ children } |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-page-container-header.component' |
@ -0,0 +1,27 @@ |
||||
.confirm-page-container-header { |
||||
display: flex; |
||||
flex-direction: column; |
||||
flex: 0 0 auto; |
||||
|
||||
&__row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
border-bottom: 1px solid $geyser; |
||||
padding: 13px 13px 13px 24px; |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__back-button-container { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__back-button { |
||||
color: #2f9ae0; |
||||
font-size: 1rem; |
||||
cursor: pointer; |
||||
font-weight: 400; |
||||
padding-left: 5px; |
||||
} |
||||
} |
@ -0,0 +1,118 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import SenderToRecipient from '../sender-to-recipient' |
||||
import { PageContainerFooter } from '../page-container' |
||||
import { ConfirmPageContainerHeader, ConfirmPageContainerContent } from './' |
||||
|
||||
export default class ConfirmPageContainer extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
// Header
|
||||
action: PropTypes.string, |
||||
hideSubtitle: PropTypes.bool, |
||||
onEdit: PropTypes.func, |
||||
showEdit: PropTypes.bool, |
||||
subtitle: PropTypes.string, |
||||
title: PropTypes.string, |
||||
titleComponent: PropTypes.func, |
||||
// Sender to Recipient
|
||||
fromAddress: PropTypes.string, |
||||
fromName: PropTypes.string, |
||||
toAddress: PropTypes.string, |
||||
toName: PropTypes.string, |
||||
// Content
|
||||
contentComponent: PropTypes.node, |
||||
errorKey: PropTypes.string, |
||||
errorMessage: PropTypes.string, |
||||
fiatTransactionAmount: PropTypes.string, |
||||
fiatTransactionFee: PropTypes.string, |
||||
fiatTransactionTotal: PropTypes.string, |
||||
ethTransactionAmount: PropTypes.string, |
||||
ethTransactionFee: PropTypes.string, |
||||
ethTransactionTotal: PropTypes.string, |
||||
onEditGas: PropTypes.func, |
||||
dataComponent: PropTypes.node, |
||||
detailsComponent: PropTypes.node, |
||||
identiconAddress: PropTypes.string, |
||||
nonce: PropTypes.string, |
||||
summaryComponent: PropTypes.node, |
||||
warning: PropTypes.string, |
||||
// Footer
|
||||
onCancel: PropTypes.func, |
||||
onSubmit: PropTypes.func, |
||||
valid: PropTypes.bool, |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
showEdit, |
||||
onEdit, |
||||
fromName, |
||||
fromAddress, |
||||
toName, |
||||
toAddress, |
||||
valid, |
||||
errorKey, |
||||
errorMessage, |
||||
contentComponent, |
||||
action, |
||||
title, |
||||
titleComponent, |
||||
subtitle, |
||||
hideSubtitle, |
||||
summaryComponent, |
||||
detailsComponent, |
||||
dataComponent, |
||||
onCancel, |
||||
onSubmit, |
||||
identiconAddress, |
||||
nonce, |
||||
warning, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<div className="page-container"> |
||||
<ConfirmPageContainerHeader |
||||
showEdit={showEdit} |
||||
onEdit={() => onEdit()} |
||||
> |
||||
<SenderToRecipient |
||||
senderName={fromName} |
||||
senderAddress={fromAddress} |
||||
recipientName={toName} |
||||
recipientAddress={toAddress} |
||||
/> |
||||
</ConfirmPageContainerHeader> |
||||
{ |
||||
contentComponent || ( |
||||
<ConfirmPageContainerContent |
||||
action={action} |
||||
title={title} |
||||
titleComponent={titleComponent} |
||||
subtitle={subtitle} |
||||
hideSubtitle={hideSubtitle} |
||||
summaryComponent={summaryComponent} |
||||
detailsComponent={detailsComponent} |
||||
dataComponent={dataComponent} |
||||
errorMessage={errorMessage} |
||||
errorKey={errorKey} |
||||
identiconAddress={identiconAddress} |
||||
nonce={nonce} |
||||
warning={warning} |
||||
/> |
||||
) |
||||
} |
||||
<PageContainerFooter |
||||
onCancel={() => onCancel()} |
||||
onSubmit={() => onSubmit()} |
||||
submitText={this.context.t('confirm')} |
||||
submitButtonType="confirm" |
||||
disabled={!valid} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,8 @@ |
||||
export { default } from './confirm-page-container.component' |
||||
export { default as ConfirmPageContainerHeader } from './confirm-page-container-header' |
||||
export { default as ConfirmDetailRow } from './confirm-detail-row' |
||||
export { |
||||
default as ConfirmPageContainerContent, |
||||
ConfirmPageContainerSummary, |
||||
ConfirmPageContainerError, |
||||
} from './confirm-page-container-content' |
@ -0,0 +1,5 @@ |
||||
@import './confirm-page-container-content/index'; |
||||
|
||||
@import './confirm-page-container-header/index'; |
||||
|
||||
@import './confirm-detail-row/index'; |
@ -0,0 +1,140 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import GasModalCard from '../../customize-gas-modal/gas-modal-card' |
||||
import { MIN_GAS_PRICE_GWEI } from '../../send_/send.constants' |
||||
|
||||
import { |
||||
getDecimalGasLimit, |
||||
getDecimalGasPrice, |
||||
getPrefixedHexGasLimit, |
||||
getPrefixedHexGasPrice, |
||||
} from './customize-gas.util' |
||||
|
||||
export default class CustomizeGas extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
txData: PropTypes.object.isRequired, |
||||
hideModal: PropTypes.func, |
||||
validate: PropTypes.func, |
||||
onSubmit: PropTypes.func, |
||||
} |
||||
|
||||
state = { |
||||
gasPrice: 0, |
||||
gasLimit: 0, |
||||
originalGasPrice: 0, |
||||
originalGasLimit: 0, |
||||
} |
||||
|
||||
componentDidMount () { |
||||
const { txData = {} } = this.props |
||||
const { txParams: { gas: hexGasLimit, gasPrice: hexGasPrice } = {} } = txData |
||||
|
||||
const gasLimit = getDecimalGasLimit(hexGasLimit) |
||||
const gasPrice = getDecimalGasPrice(hexGasPrice) |
||||
|
||||
this.setState({ |
||||
gasPrice, |
||||
gasLimit, |
||||
originalGasPrice: gasPrice, |
||||
originalGasLimit: gasLimit, |
||||
}) |
||||
} |
||||
|
||||
handleRevert () { |
||||
const { originalGasPrice, originalGasLimit } = this.state |
||||
|
||||
this.setState({ |
||||
gasPrice: originalGasPrice, |
||||
gasLimit: originalGasLimit, |
||||
}) |
||||
} |
||||
|
||||
handleSave () { |
||||
const { onSubmit, hideModal } = this.props |
||||
const { gasLimit, gasPrice } = this.state |
||||
const prefixedHexGasPrice = getPrefixedHexGasPrice(gasPrice) |
||||
const prefixedHexGasLimit = getPrefixedHexGasLimit(gasLimit) |
||||
|
||||
Promise.resolve(onSubmit({ gasPrice: prefixedHexGasPrice, gasLimit: prefixedHexGasLimit })) |
||||
.then(() => hideModal()) |
||||
} |
||||
|
||||
validate () { |
||||
const { gasLimit, gasPrice } = this.state |
||||
return this.props.validate({ |
||||
gasPrice: getPrefixedHexGasPrice(gasPrice), |
||||
gasLimit: getPrefixedHexGasLimit(gasLimit), |
||||
}) |
||||
} |
||||
|
||||
render () { |
||||
const { t } = this.context |
||||
const { hideModal } = this.props |
||||
const { gasPrice, gasLimit } = this.state |
||||
const { valid, errorKey } = this.validate() |
||||
|
||||
return ( |
||||
<div className="customize-gas"> |
||||
<div className="customize-gas__content"> |
||||
<div className="customize-gas__header"> |
||||
<div className="customize-gas__title"> |
||||
{ this.context.t('customGas') } |
||||
</div> |
||||
<div |
||||
className="customize-gas__close" |
||||
onClick={() => hideModal()} |
||||
/> |
||||
</div> |
||||
<div className="customize-gas__body"> |
||||
<GasModalCard |
||||
value={gasPrice} |
||||
min={MIN_GAS_PRICE_GWEI} |
||||
step={1} |
||||
onChange={value => this.setState({ gasPrice: value })} |
||||
title={t('gasPrice')} |
||||
copy={t('gasPriceCalculation')} |
||||
/> |
||||
<GasModalCard |
||||
value={gasLimit} |
||||
min={1} |
||||
step={1} |
||||
onChange={value => this.setState({ gasLimit: value })} |
||||
title={t('gasLimit')} |
||||
copy={t('gasLimitCalculation')} |
||||
/> |
||||
</div> |
||||
<div className="customize-gas__footer"> |
||||
{ !valid && <div className="customize-gas__error-message">{ t(errorKey) }</div> } |
||||
<div |
||||
className="customize-gas__revert" |
||||
onClick={() => this.handleRevert()} |
||||
> |
||||
{ t('revert') } |
||||
</div> |
||||
<div className="customize-gas__buttons"> |
||||
<button |
||||
className="btn-default customize-gas__cancel" |
||||
onClick={() => hideModal()} |
||||
style={{ marginRight: '10px' }} |
||||
> |
||||
{ t('cancel') } |
||||
</button> |
||||
<button |
||||
className="btn-primary customize-gas__save" |
||||
onClick={() => this.handleSave()} |
||||
style={{ marginRight: '10px' }} |
||||
disabled={!valid} |
||||
> |
||||
{ t('save') } |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
import { connect } from 'react-redux' |
||||
import CustomizeGas from './customize-gas.component' |
||||
import { hideModal } from '../../../actions' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { appState: { modal: { modalState: { props } } } } = state |
||||
const { txData, onSubmit, validate } = props |
||||
|
||||
return { |
||||
txData, |
||||
onSubmit, |
||||
validate, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
hideModal: () => dispatch(hideModal()), |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CustomizeGas) |
@ -0,0 +1,34 @@ |
||||
import ethUtil from 'ethereumjs-util' |
||||
import { conversionUtil } from '../../../conversion-util' |
||||
|
||||
export function getDecimalGasLimit (hexGasLimit) { |
||||
return conversionUtil(hexGasLimit, { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
}) |
||||
} |
||||
|
||||
export function getDecimalGasPrice (hexGasPrice) { |
||||
return conversionUtil(hexGasPrice, { |
||||
fromNumericBase: 'hex', |
||||
toNumericBase: 'dec', |
||||
fromDenomination: 'WEI', |
||||
toDenomination: 'GWEI', |
||||
}) |
||||
} |
||||
|
||||
export function getPrefixedHexGasLimit (gasLimit) { |
||||
return ethUtil.addHexPrefix(conversionUtil(gasLimit, { |
||||
fromNumericBase: 'dec', |
||||
toNumericBase: 'hex', |
||||
})) |
||||
} |
||||
|
||||
export function getPrefixedHexGasPrice (gasPrice) { |
||||
return ethUtil.addHexPrefix(conversionUtil(gasPrice, { |
||||
fromNumericBase: 'dec', |
||||
toNumericBase: 'hex', |
||||
fromDenomination: 'GWEI', |
||||
toDenomination: 'WEI', |
||||
})) |
||||
} |
@ -0,0 +1 @@ |
||||
export { default } from './customize-gas.container' |
@ -0,0 +1,110 @@ |
||||
.customize-gas { |
||||
border: 1px solid #D8D8D8; |
||||
border-radius: 4px; |
||||
background-color: #FFFFFF; |
||||
box-shadow: 0 2px 4px 0 rgba(0,0,0,0.14); |
||||
font-family: Roboto; |
||||
display: flex; |
||||
flex-flow: column; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
width: 100vw; |
||||
height: 100vh; |
||||
} |
||||
|
||||
&__header { |
||||
height: 52px; |
||||
border-bottom: 1px solid $alto; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
font-size: 22px; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
flex: 0 0 auto; |
||||
} |
||||
} |
||||
|
||||
&__title { |
||||
margin-left: 19.25px; |
||||
} |
||||
|
||||
&__close::after { |
||||
content: '\00D7'; |
||||
font-size: 1.8em; |
||||
color: $dusty-gray; |
||||
font-family: sans-serif; |
||||
cursor: pointer; |
||||
margin-right: 19.25px; |
||||
} |
||||
|
||||
&__content { |
||||
display: flex; |
||||
flex-flow: column nowrap; |
||||
height: 100%; |
||||
} |
||||
|
||||
&__body { |
||||
display: flex; |
||||
margin-bottom: 24px; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
flex-flow: column; |
||||
flex: 1 1 auto; |
||||
} |
||||
} |
||||
|
||||
&__footer { |
||||
height: 75px; |
||||
border-top: 1px solid $alto; |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
font-size: 22px; |
||||
position: relative; |
||||
|
||||
@media screen and (max-width: $break-small) { |
||||
flex: 0 0 auto; |
||||
} |
||||
} |
||||
|
||||
&__buttons { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
margin-right: 21.25px; |
||||
} |
||||
|
||||
&__revert, &__cancel, &__save, &__save__error { |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
padding: 0 3px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
&__revert { |
||||
color: $silver-chalice; |
||||
font-size: 16px; |
||||
margin-left: 21.25px; |
||||
} |
||||
|
||||
&__cancel, &__save, &__save__error { |
||||
width: 85.74px; |
||||
min-width: initial; |
||||
} |
||||
|
||||
&__save__error { |
||||
opacity: 0.5; |
||||
cursor: auto; |
||||
} |
||||
|
||||
&__error-message { |
||||
display: block; |
||||
position: absolute; |
||||
top: 4px; |
||||
right: 4px; |
||||
font-size: 12px; |
||||
line-height: 12px; |
||||
color: $red; |
||||
} |
||||
} |
@ -1,56 +0,0 @@ |
||||
const { Component } = require('react') |
||||
const h = require('react-hyperscript') |
||||
const PropTypes = require('prop-types') |
||||
const connect = require('react-redux').connect |
||||
const NetworkDropdownIcon = require('./dropdowns/components/network-dropdown-icon') |
||||
|
||||
const networkToColorHash = { |
||||
1: '#038789', |
||||
3: '#e91550', |
||||
42: '#690496', |
||||
4: '#ebb33f', |
||||
} |
||||
|
||||
class NetworkDisplay extends Component { |
||||
renderNetworkIcon () { |
||||
const { network } = this.props |
||||
const networkColor = networkToColorHash[network] |
||||
|
||||
return networkColor |
||||
? h(NetworkDropdownIcon, { backgroundColor: networkColor }) |
||||
: h('i.fa.fa-question-circle.fa-med', { |
||||
style: { |
||||
margin: '0 4px', |
||||
color: 'rgb(125, 128, 130)', |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
render () { |
||||
const { provider: { type } } = this.props |
||||
return h('.network-display__container', [ |
||||
this.renderNetworkIcon(), |
||||
h('.network-name', this.context.t(type)), |
||||
]) |
||||
} |
||||
} |
||||
|
||||
NetworkDisplay.propTypes = { |
||||
network: PropTypes.string, |
||||
provider: PropTypes.object, |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
const mapStateToProps = ({ metamask: { network, provider } }) => { |
||||
return { |
||||
network, |
||||
provider, |
||||
} |
||||
} |
||||
|
||||
NetworkDisplay.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = connect(mapStateToProps)(NetworkDisplay) |
||||
|
@ -0,0 +1,2 @@ |
||||
import NetworkDisplay from './network-display.container' |
||||
module.exports = NetworkDisplay |
@ -0,0 +1,54 @@ |
||||
.network-display { |
||||
&__container { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: flex-start; |
||||
background-color: lighten(rgb(125, 128, 130), 45%); |
||||
padding: 0 10px; |
||||
border-radius: 4px; |
||||
height: 25px; |
||||
|
||||
&--mainnet { |
||||
background-color: lighten($blue-lagoon, 45%); |
||||
} |
||||
|
||||
&--ropsten { |
||||
background-color: lighten($crimson, 45%); |
||||
} |
||||
|
||||
&--kovan { |
||||
background-color: lighten($purple, 45%); |
||||
} |
||||
|
||||
&--rinkeby { |
||||
background-color: lighten($tulip-tree, 45%); |
||||
} |
||||
} |
||||
|
||||
&__name { |
||||
font-size: .75rem; |
||||
padding-left: 5px; |
||||
} |
||||
|
||||
&__icon { |
||||
height: 10px; |
||||
width: 10px; |
||||
border-radius: 10px; |
||||
|
||||
&--mainnet { |
||||
background-color: $blue-lagoon; |
||||
} |
||||
|
||||
&--ropsten { |
||||
background-color: $crimson; |
||||
} |
||||
|
||||
&--kovan { |
||||
background-color: $purple; |
||||
} |
||||
|
||||
&--rinkeby { |
||||
background-color: $tulip-tree; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,69 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import classnames from 'classnames' |
||||
import { |
||||
MAINNET_CODE, |
||||
ROPSTEN_CODE, |
||||
RINKEYBY_CODE, |
||||
KOVAN_CODE, |
||||
} from '../../../../app/scripts/controllers/network/enums' |
||||
|
||||
const networkToClassHash = { |
||||
[MAINNET_CODE]: 'mainnet', |
||||
[ROPSTEN_CODE]: 'ropsten', |
||||
[RINKEYBY_CODE]: 'rinkeby', |
||||
[KOVAN_CODE]: 'kovan', |
||||
} |
||||
|
||||
export default class NetworkDisplay extends Component { |
||||
static propTypes = { |
||||
network: PropTypes.string, |
||||
provider: PropTypes.object, |
||||
} |
||||
|
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
renderNetworkIcon () { |
||||
const { network } = this.props |
||||
const networkClass = networkToClassHash[network] |
||||
|
||||
return networkClass |
||||
? <div className={`network-display__icon network-display__icon--${networkClass}`} /> |
||||
: <div |
||||
className="i fa fa-question-circle fa-med" |
||||
style={{ |
||||
margin: '0 4px', |
||||
color: 'rgb(125, 128, 130)', |
||||
}} |
||||
/> |
||||
} |
||||
|
||||
render () { |
||||
const { network, provider: { type } } = this.props |
||||
const networkClass = networkToClassHash[network] |
||||
|
||||
return ( |
||||
<div className={classnames( |
||||
'network-display__container', |
||||
networkClass && ('network-display__container--' + networkClass) |
||||
)}> |
||||
{ |
||||
networkClass |
||||
? <div className={`network-display__icon network-display__icon--${networkClass}`} /> |
||||
: <div |
||||
className="i fa fa-question-circle fa-med" |
||||
style={{ |
||||
margin: '0 4px', |
||||
color: 'rgb(125, 128, 130)', |
||||
}} |
||||
/> |
||||
} |
||||
<div className="network-display__name"> |
||||
{ this.context.t(type) } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
import { connect } from 'react-redux' |
||||
import NetworkDisplay from './network-display.component' |
||||
|
||||
const mapStateToProps = ({ metamask: { network, provider } }) => { |
||||
return { |
||||
network, |
||||
provider, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(NetworkDisplay) |
@ -1 +1,4 @@ |
||||
import PageContainerHeader from './page-container-header' |
||||
import PageContainerFooter from './page-container-footer' |
||||
export { default } from './page-container.component' |
||||
export { PageContainerHeader, PageContainerFooter } |
||||
|
@ -0,0 +1,186 @@ |
||||
.page-container { |
||||
width: 408px; |
||||
background-color: $white; |
||||
box-shadow: 0 0 7px 0 rgba(0, 0, 0, .08); |
||||
z-index: 25; |
||||
display: flex; |
||||
flex-flow: column; |
||||
border-radius: 8px; |
||||
|
||||
&__header { |
||||
display: flex; |
||||
flex-flow: column; |
||||
border-bottom: 1px solid $geyser; |
||||
padding: 16px; |
||||
flex: 0 0 auto; |
||||
position: relative; |
||||
|
||||
&--no-padding-bottom { |
||||
padding-bottom: 0; |
||||
} |
||||
} |
||||
|
||||
&__header-close { |
||||
color: $tundora; |
||||
position: absolute; |
||||
top: 16px; |
||||
right: 16px; |
||||
cursor: pointer; |
||||
overflow: hidden; |
||||
|
||||
&::after { |
||||
content: '\00D7'; |
||||
font-size: 40px; |
||||
line-height: 20px; |
||||
} |
||||
} |
||||
|
||||
&__header-row { |
||||
padding-bottom: 10px; |
||||
display: flex; |
||||
justify-content: space-between; |
||||
} |
||||
|
||||
&__footer { |
||||
display: flex; |
||||
flex-flow: row; |
||||
justify-content: center; |
||||
border-top: 1px solid $geyser; |
||||
padding: 16px; |
||||
flex: 0 0 auto; |
||||
|
||||
.btn-default, |
||||
.btn-confirm { |
||||
font-size: 1rem; |
||||
} |
||||
} |
||||
|
||||
&__footer-button { |
||||
height: 55px; |
||||
font-size: 1rem; |
||||
text-transform: uppercase; |
||||
margin-right: 16px; |
||||
|
||||
&:last-of-type { |
||||
margin-right: 0; |
||||
} |
||||
} |
||||
|
||||
&__back-button { |
||||
color: #2f9ae0; |
||||
font-size: 1rem; |
||||
cursor: pointer; |
||||
font-weight: 400; |
||||
} |
||||
|
||||
&__title { |
||||
color: $black; |
||||
font-size: 2rem; |
||||
font-weight: 500; |
||||
line-height: 2rem; |
||||
} |
||||
|
||||
&__subtitle { |
||||
padding-top: .5rem; |
||||
line-height: initial; |
||||
font-size: .9rem; |
||||
color: $gray; |
||||
} |
||||
|
||||
&__tabs { |
||||
display: flex; |
||||
margin-top: 16px; |
||||
} |
||||
|
||||
&__tab { |
||||
min-width: 5rem; |
||||
padding: 8px; |
||||
color: $dusty-gray; |
||||
font-family: Roboto; |
||||
font-size: 1rem; |
||||
text-align: center; |
||||
cursor: pointer; |
||||
border-bottom: none; |
||||
margin-right: 16px; |
||||
|
||||
&:last-of-type { |
||||
margin-right: 0; |
||||
} |
||||
|
||||
&--selected { |
||||
color: $curious-blue; |
||||
border-bottom: 3px solid $curious-blue; |
||||
} |
||||
} |
||||
|
||||
&--full-width { |
||||
width: 100% !important; |
||||
} |
||||
|
||||
&--full-height { |
||||
height: 100% !important; |
||||
max-height: initial !important; |
||||
min-height: initial !important; |
||||
} |
||||
|
||||
&__content { |
||||
overflow-y: auto; |
||||
flex: 1; |
||||
} |
||||
|
||||
&__warning-container { |
||||
background: $linen; |
||||
padding: 20px; |
||||
display: flex; |
||||
align-items: start; |
||||
} |
||||
|
||||
&__warning-message { |
||||
padding-left: 15px; |
||||
} |
||||
|
||||
&__warning-title { |
||||
font-weight: 500; |
||||
} |
||||
|
||||
&__warning-icon { |
||||
padding-top: 5px; |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 250px) { |
||||
.page-container { |
||||
&__footer { |
||||
flex-flow: column-reverse; |
||||
} |
||||
|
||||
&__footer-button { |
||||
width: 100%; |
||||
margin-bottom: 1rem; |
||||
margin-right: 0; |
||||
|
||||
&:first-of-type { |
||||
margin-bottom: 0; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 575px) { |
||||
.page-container { |
||||
height: 100%; |
||||
width: 100%; |
||||
overflow-y: auto; |
||||
background-color: $white; |
||||
border-radius: 0; |
||||
flex: 1; |
||||
} |
||||
} |
||||
|
||||
@media screen and (min-width: 576px) { |
||||
.page-container { |
||||
max-height: 82vh; |
||||
min-height: 570px; |
||||
flex: 0 0 auto; |
||||
} |
||||
} |
@ -1,35 +0,0 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
|
||||
export default class PageContainerHeader extends Component { |
||||
|
||||
static propTypes = { |
||||
title: PropTypes.string, |
||||
subtitle: PropTypes.string, |
||||
onClose: PropTypes.func, |
||||
}; |
||||
|
||||
render () { |
||||
const { title, subtitle, onClose } = this.props |
||||
|
||||
return ( |
||||
<div className="page-container__header"> |
||||
|
||||
<div className="page-container__title"> |
||||
{title} |
||||
</div> |
||||
|
||||
<div className="page-container__subtitle"> |
||||
{subtitle} |
||||
</div> |
||||
|
||||
<div |
||||
className="page-container__header-close" |
||||
onClick={() => onClose()} |
||||
/> |
||||
|
||||
</div> |
||||
) |
||||
} |
||||
|
||||
} |
@ -0,0 +1,30 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
|
||||
export default class ConfirmApprove extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
tokenAddress: PropTypes.string, |
||||
toAddress: PropTypes.string, |
||||
tokenAmount: PropTypes.string, |
||||
tokenSymbol: PropTypes.string, |
||||
} |
||||
|
||||
render () { |
||||
const { toAddress, tokenAddress, tokenAmount, tokenSymbol } = this.props |
||||
|
||||
return ( |
||||
<ConfirmTransactionBase |
||||
toAddress={toAddress} |
||||
identiconAddress={tokenAddress} |
||||
title={`${tokenAmount} ${tokenSymbol}`} |
||||
warning={`By approving this action, you grant permission for this contract to spend up to ${tokenAmount} of your ${tokenSymbol}.`} |
||||
hideSubtitle |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
import { connect } from 'react-redux' |
||||
import ConfirmApprove from './confirm-approve.component' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { confirmTransaction } = state |
||||
const { |
||||
tokenData = {}, |
||||
txData: { txParams: { to: tokenAddress } = {} } = {}, |
||||
tokenProps: { tokenSymbol } = {}, |
||||
} = confirmTransaction |
||||
const { params = [] } = tokenData |
||||
|
||||
let toAddress = '' |
||||
let tokenAmount = '' |
||||
|
||||
if (params && params.length === 2) { |
||||
[{ value: toAddress }, { value: tokenAmount }] = params |
||||
} |
||||
|
||||
return { |
||||
toAddress, |
||||
tokenAddress, |
||||
tokenAmount, |
||||
tokenSymbol, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(ConfirmApprove) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-approve.container' |
@ -0,0 +1,64 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ethUtil from 'ethereumjs-util' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
|
||||
export default class ConfirmDeployContract extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
txData: PropTypes.object, |
||||
} |
||||
|
||||
renderData () { |
||||
const { t } = this.context |
||||
const { |
||||
txData: { |
||||
origin, |
||||
txParams: { |
||||
data, |
||||
} = {}, |
||||
} = {}, |
||||
} = this.props |
||||
|
||||
return ( |
||||
<div className="confirm-page-container-content__data"> |
||||
<div className="confirm-page-container-content__data-box"> |
||||
<div className="confirm-page-container-content__data-field"> |
||||
<div className="confirm-page-container-content__data-field-label"> |
||||
{ `${t('origin')}:` } |
||||
</div> |
||||
<div> |
||||
{ origin } |
||||
</div> |
||||
</div> |
||||
<div className="confirm-page-container-content__data-field"> |
||||
<div className="confirm-page-container-content__data-field-label"> |
||||
{ `${t('bytes')}:` } |
||||
</div> |
||||
<div> |
||||
{ ethUtil.toBuffer(data).length } |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div className="confirm-page-container-content__data-box-label"> |
||||
{ `${t('hexData')}:` } |
||||
</div> |
||||
<div className="confirm-page-container-content__data-box"> |
||||
{ data } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
return ( |
||||
<ConfirmTransactionBase |
||||
action={this.context.t('contractDeployment')} |
||||
dataComponent={this.renderData()} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,12 @@ |
||||
import { connect } from 'react-redux' |
||||
import ConfirmDeployContract from './confirm-deploy-contract.component' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { confirmTransaction: { txData } = {} } = state |
||||
|
||||
return { |
||||
txData, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(ConfirmDeployContract) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-deploy-contract.container' |
@ -0,0 +1,39 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
import { SEND_ROUTE } from '../../../routes' |
||||
|
||||
export default class ConfirmSendEther extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
editTransaction: PropTypes.func, |
||||
history: PropTypes.object, |
||||
txParams: PropTypes.object, |
||||
} |
||||
|
||||
handleEdit ({ txData }) { |
||||
const { editTransaction, history } = this.props |
||||
editTransaction(txData) |
||||
history.push(SEND_ROUTE) |
||||
} |
||||
|
||||
shouldHideData () { |
||||
const { txParams = {} } = this.props |
||||
return !txParams.data |
||||
} |
||||
|
||||
render () { |
||||
const hideData = this.shouldHideData() |
||||
|
||||
return ( |
||||
<ConfirmTransactionBase |
||||
action={this.context.t('confirm')} |
||||
hideData={hideData} |
||||
onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,45 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { updateSend } from '../../../actions' |
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' |
||||
import ConfirmSendEther from './confirm-send-ether.component' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { confirmTransaction: { txData: { txParams } = {} } } = state |
||||
|
||||
return { |
||||
txParams, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
editTransaction: txData => { |
||||
const { id, txParams } = txData |
||||
const { |
||||
gas: gasLimit, |
||||
gasPrice, |
||||
to, |
||||
value: amount, |
||||
} = txParams |
||||
|
||||
dispatch(updateSend({ |
||||
gasLimit, |
||||
gasPrice, |
||||
gasTotal: null, |
||||
to, |
||||
amount, |
||||
errors: { to: null, amount: null }, |
||||
editingTransactionId: id && id.toString(), |
||||
})) |
||||
|
||||
dispatch(clearConfirmTransaction()) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(ConfirmSendEther) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-send-ether.container' |
@ -0,0 +1,39 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
import { SEND_ROUTE } from '../../../routes' |
||||
|
||||
export default class ConfirmSendToken extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
history: PropTypes.object, |
||||
tokenAddress: PropTypes.string, |
||||
toAddress: PropTypes.string, |
||||
numberOfTokens: PropTypes.number, |
||||
tokenSymbol: PropTypes.string, |
||||
editTransaction: PropTypes.func, |
||||
} |
||||
|
||||
handleEdit (confirmTransactionData) { |
||||
const { editTransaction, history } = this.props |
||||
editTransaction(confirmTransactionData) |
||||
history.push(SEND_ROUTE) |
||||
} |
||||
|
||||
render () { |
||||
const { toAddress, tokenAddress, tokenSymbol, numberOfTokens } = this.props |
||||
|
||||
return ( |
||||
<ConfirmTransactionBase |
||||
toAddress={toAddress} |
||||
identiconAddress={tokenAddress} |
||||
title={`${numberOfTokens} ${tokenSymbol}`} |
||||
onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} |
||||
hideSubtitle |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,72 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import ConfirmSendToken from './confirm-send-token.component' |
||||
import { calcTokenAmount } from '../../../token-util' |
||||
import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' |
||||
import { setSelectedToken, updateSend, showSendTokenPage } from '../../../actions' |
||||
import { conversionUtil } from '../../../conversion-util' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { confirmTransaction } = state |
||||
const { |
||||
tokenData = {}, |
||||
tokenProps: { tokenSymbol, tokenDecimals } = {}, |
||||
txData: { txParams: { to: tokenAddress } = {} } = {}, |
||||
} = confirmTransaction |
||||
const { params = [] } = tokenData |
||||
|
||||
let toAddress = '' |
||||
let tokenAmount = '' |
||||
|
||||
if (params && params.length === 2) { |
||||
[{ value: toAddress }, { value: tokenAmount }] = params |
||||
} |
||||
|
||||
const numberOfTokens = tokenAmount && tokenDecimals |
||||
? calcTokenAmount(tokenAmount, tokenDecimals) |
||||
: 0 |
||||
|
||||
return { |
||||
toAddress, |
||||
tokenAddress, |
||||
tokenSymbol, |
||||
numberOfTokens, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
editTransaction: ({ txData, tokenData, tokenProps }) => { |
||||
const { txParams: { to: tokenAddress, gas: gasLimit, gasPrice } = {}, id } = txData |
||||
const { params = [] } = tokenData |
||||
const { value: to } = params[0] || {} |
||||
const { value: tokenAmountInDec } = params[1] || {} |
||||
const tokenAmountInHex = conversionUtil(tokenAmountInDec, { |
||||
fromNumericBase: 'dec', |
||||
toNumericBase: 'hex', |
||||
}) |
||||
dispatch(setSelectedToken(tokenAddress)) |
||||
dispatch(updateSend({ |
||||
gasLimit, |
||||
gasPrice, |
||||
gasTotal: null, |
||||
to, |
||||
amount: tokenAmountInHex, |
||||
errors: { to: null, amount: null }, |
||||
editingTransactionId: id && id.toString(), |
||||
token: { |
||||
...tokenProps, |
||||
address: tokenAddress, |
||||
}, |
||||
})) |
||||
dispatch(clearConfirmTransaction()) |
||||
dispatch(showSendTokenPage()) |
||||
}, |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps) |
||||
)(ConfirmSendToken) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-send-token.container' |
@ -0,0 +1,19 @@ |
||||
.confirm-send-token { |
||||
&__title { |
||||
padding: 4px 0; |
||||
display: flex; |
||||
align-items: center; |
||||
} |
||||
|
||||
&__identicon { |
||||
flex: 0 0 auto; |
||||
} |
||||
|
||||
&__title-text { |
||||
font-size: 2.25rem; |
||||
white-space: nowrap; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
padding-left: 8px; |
||||
} |
||||
} |
@ -0,0 +1,320 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' |
||||
import { formatCurrency } from '../../../helpers/confirm-transaction/util' |
||||
import { isBalanceSufficient } from '../../send_/send.utils' |
||||
import { DEFAULT_ROUTE } from '../../../routes' |
||||
import { |
||||
INSUFFICIENT_FUNDS_ERROR_KEY, |
||||
TRANSACTION_ERROR_KEY, |
||||
} from '../../../constants/error-keys' |
||||
|
||||
export default class ConfirmTransactionBase extends Component { |
||||
static contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
static propTypes = { |
||||
// react-router props
|
||||
match: PropTypes.object, |
||||
history: PropTypes.object, |
||||
// Redux props
|
||||
balance: PropTypes.string, |
||||
cancelTransaction: PropTypes.func, |
||||
clearConfirmTransaction: PropTypes.func, |
||||
clearSend: PropTypes.func, |
||||
conversionRate: PropTypes.number, |
||||
currentCurrency: PropTypes.string, |
||||
editTransaction: PropTypes.func, |
||||
ethTransactionAmount: PropTypes.string, |
||||
ethTransactionFee: PropTypes.string, |
||||
ethTransactionTotal: PropTypes.string, |
||||
fiatTransactionAmount: PropTypes.string, |
||||
fiatTransactionFee: PropTypes.string, |
||||
fiatTransactionTotal: PropTypes.string, |
||||
fromAddress: PropTypes.string, |
||||
fromName: PropTypes.string, |
||||
hexGasTotal: PropTypes.string, |
||||
isTxReprice: PropTypes.bool, |
||||
methodData: PropTypes.object, |
||||
nonce: PropTypes.string, |
||||
sendTransaction: PropTypes.func, |
||||
showCustomizeGasModal: PropTypes.func, |
||||
showTransactionConfirmedModal: PropTypes.func, |
||||
toAddress: PropTypes.string, |
||||
tokenData: PropTypes.object, |
||||
tokenProps: PropTypes.object, |
||||
toName: PropTypes.string, |
||||
transactionStatus: PropTypes.string, |
||||
txData: PropTypes.object, |
||||
// Component props
|
||||
action: PropTypes.string, |
||||
contentComponent: PropTypes.node, |
||||
dataComponent: PropTypes.node, |
||||
detailsComponent: PropTypes.node, |
||||
errorKey: PropTypes.string, |
||||
errorMessage: PropTypes.string, |
||||
hideData: PropTypes.bool, |
||||
hideDetails: PropTypes.bool, |
||||
hideSubtitle: PropTypes.bool, |
||||
identiconAddress: PropTypes.string, |
||||
onCancel: PropTypes.func, |
||||
onEdit: PropTypes.func, |
||||
onEditGas: PropTypes.func, |
||||
onSubmit: PropTypes.func, |
||||
subtitle: PropTypes.string, |
||||
summaryComponent: PropTypes.node, |
||||
title: PropTypes.string, |
||||
valid: PropTypes.bool, |
||||
warning: PropTypes.string, |
||||
} |
||||
|
||||
componentDidUpdate () { |
||||
const { |
||||
transactionStatus, |
||||
showTransactionConfirmedModal, |
||||
history, |
||||
clearConfirmTransaction, |
||||
} = this.props |
||||
|
||||
if (transactionStatus === 'dropped') { |
||||
showTransactionConfirmedModal({ |
||||
onHide: () => { |
||||
clearConfirmTransaction() |
||||
history.push(DEFAULT_ROUTE) |
||||
}, |
||||
}) |
||||
|
||||
return |
||||
} |
||||
} |
||||
|
||||
getErrorKey () { |
||||
const { |
||||
balance, |
||||
conversionRate, |
||||
hexGasTotal, |
||||
txData: { |
||||
simulationFails, |
||||
txParams: { |
||||
value: amount, |
||||
} = {}, |
||||
} = {}, |
||||
} = this.props |
||||
|
||||
const insufficientBalance = balance && !isBalanceSufficient({ |
||||
amount, |
||||
gasTotal: hexGasTotal || '0x0', |
||||
balance, |
||||
conversionRate, |
||||
}) |
||||
|
||||
if (insufficientBalance) { |
||||
return { |
||||
valid: false, |
||||
errorKey: INSUFFICIENT_FUNDS_ERROR_KEY, |
||||
} |
||||
} |
||||
|
||||
if (simulationFails) { |
||||
return { |
||||
valid: false, |
||||
errorKey: TRANSACTION_ERROR_KEY, |
||||
} |
||||
} |
||||
|
||||
return { |
||||
valid: true, |
||||
} |
||||
} |
||||
|
||||
handleEditGas () { |
||||
const { onEditGas, showCustomizeGasModal } = this.props |
||||
|
||||
if (onEditGas) { |
||||
onEditGas() |
||||
} else { |
||||
showCustomizeGasModal() |
||||
} |
||||
} |
||||
|
||||
renderDetails () { |
||||
const { |
||||
detailsComponent, |
||||
fiatTransactionFee, |
||||
ethTransactionFee, |
||||
currentCurrency, |
||||
fiatTransactionTotal, |
||||
ethTransactionTotal, |
||||
hideDetails, |
||||
} = this.props |
||||
|
||||
if (hideDetails) { |
||||
return null |
||||
} |
||||
|
||||
return ( |
||||
detailsComponent || ( |
||||
<div className="confirm-page-container-content__details"> |
||||
<div className="confirm-page-container-content__gas-fee"> |
||||
<ConfirmDetailRow |
||||
label="Gas Fee" |
||||
fiatFee={formatCurrency(fiatTransactionFee, currentCurrency)} |
||||
ethFee={ethTransactionFee} |
||||
headerText="Edit" |
||||
headerTextClassName="confirm-detail-row__header-text--edit" |
||||
onHeaderClick={() => this.handleEditGas()} |
||||
/> |
||||
</div> |
||||
<div> |
||||
<ConfirmDetailRow |
||||
label="Total" |
||||
fiatFee={formatCurrency(fiatTransactionTotal, currentCurrency)} |
||||
ethFee={ethTransactionTotal} |
||||
headerText="Amount + Gas Fee" |
||||
headerTextClassName="confirm-detail-row__header-text--total" |
||||
fiatFeeColor="#2f9ae0" |
||||
/> |
||||
</div> |
||||
</div> |
||||
) |
||||
) |
||||
} |
||||
|
||||
renderData () { |
||||
const { t } = this.context |
||||
const { |
||||
txData: { |
||||
txParams: { |
||||
data, |
||||
} = {}, |
||||
} = {}, |
||||
methodData: { |
||||
name, |
||||
params, |
||||
} = {}, |
||||
hideData, |
||||
dataComponent, |
||||
} = this.props |
||||
|
||||
if (hideData) { |
||||
return null |
||||
} |
||||
|
||||
return dataComponent || ( |
||||
<div className="confirm-page-container-content__data"> |
||||
<div className="confirm-page-container-content__data-box-label"> |
||||
{`${t('functionType')}:`} |
||||
<span className="confirm-page-container-content__function-type"> |
||||
{ name } |
||||
</span> |
||||
</div> |
||||
<div className="confirm-page-container-content__data-box"> |
||||
<div className="confirm-page-container-content__data-field-label"> |
||||
{ `${t('parameters')}:` } |
||||
</div> |
||||
<div> |
||||
<pre>{ JSON.stringify(params, null, 2) }</pre> |
||||
</div> |
||||
</div> |
||||
<div className="confirm-page-container-content__data-box-label"> |
||||
{`${t('hexData')}:`} |
||||
</div> |
||||
<div className="confirm-page-container-content__data-box"> |
||||
{ data } |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
handleEdit () { |
||||
const { txData, tokenData, tokenProps, onEdit } = this.props |
||||
onEdit({ txData, tokenData, tokenProps }) |
||||
} |
||||
|
||||
handleCancel () { |
||||
const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props |
||||
|
||||
if (onCancel) { |
||||
onCancel(txData) |
||||
} else { |
||||
cancelTransaction(txData) |
||||
.then(() => { |
||||
clearConfirmTransaction() |
||||
history.push(DEFAULT_ROUTE) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
handleSubmit () { |
||||
const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props |
||||
|
||||
if (onSubmit) { |
||||
onSubmit(txData) |
||||
} else { |
||||
sendTransaction(txData) |
||||
.then(() => { |
||||
clearConfirmTransaction() |
||||
history.push(DEFAULT_ROUTE) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { |
||||
isTxReprice, |
||||
fromName, |
||||
fromAddress, |
||||
toName, |
||||
toAddress, |
||||
methodData, |
||||
ethTransactionAmount, |
||||
fiatTransactionAmount, |
||||
valid: propsValid, |
||||
errorMessage, |
||||
errorKey: propsErrorKey, |
||||
currentCurrency, |
||||
action, |
||||
title, |
||||
subtitle, |
||||
hideSubtitle, |
||||
identiconAddress, |
||||
summaryComponent, |
||||
contentComponent, |
||||
onEdit, |
||||
nonce, |
||||
warning, |
||||
} = this.props |
||||
|
||||
const { name } = methodData |
||||
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) |
||||
const { valid, errorKey } = this.getErrorKey() |
||||
|
||||
return ( |
||||
<ConfirmPageContainer |
||||
fromName={fromName} |
||||
fromAddress={fromAddress} |
||||
toName={toName} |
||||
toAddress={toAddress} |
||||
showEdit={onEdit && !isTxReprice} |
||||
action={action || name} |
||||
title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} |
||||
subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} |
||||
hideSubtitle={hideSubtitle} |
||||
summaryComponent={summaryComponent} |
||||
detailsComponent={this.renderDetails()} |
||||
dataComponent={this.renderData()} |
||||
contentComponent={contentComponent} |
||||
nonce={nonce} |
||||
identiconAddress={identiconAddress} |
||||
errorMessage={errorMessage} |
||||
errorKey={propsErrorKey || errorKey} |
||||
warning={warning} |
||||
valid={propsValid || valid} |
||||
onEdit={() => this.handleEdit()} |
||||
onCancel={() => this.handleCancel()} |
||||
onSubmit={() => this.handleSubmit()} |
||||
/> |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,169 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import R from 'ramda' |
||||
import ConfirmTransactionBase from './confirm-transaction-base.component' |
||||
import { |
||||
clearConfirmTransaction, |
||||
updateGasAndCalculate, |
||||
} from '../../../ducks/confirm-transaction.duck' |
||||
import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions' |
||||
import { |
||||
INSUFFICIENT_FUNDS_ERROR_KEY, |
||||
GAS_LIMIT_TOO_LOW_ERROR_KEY, |
||||
} from '../../../constants/error-keys' |
||||
import { getHexGasTotal } from '../../../helpers/confirm-transaction/util' |
||||
import { isBalanceSufficient } from '../../send_/send.utils' |
||||
import { conversionGreaterThan } from '../../../conversion-util' |
||||
import { MIN_GAS_LIMIT_DEC } from '../../send_/send.constants' |
||||
|
||||
const mapStateToProps = (state, props) => { |
||||
const { toAddress: propsToAddress } = props |
||||
const { confirmTransaction, metamask } = state |
||||
const { |
||||
ethTransactionAmount, |
||||
ethTransactionFee, |
||||
ethTransactionTotal, |
||||
fiatTransactionAmount, |
||||
fiatTransactionFee, |
||||
fiatTransactionTotal, |
||||
hexGasTotal, |
||||
tokenData, |
||||
methodData, |
||||
txData, |
||||
tokenProps, |
||||
nonce, |
||||
} = confirmTransaction |
||||
const { txParams = {}, lastGasPrice, id: transactionId } = txData |
||||
const { from: fromAddress, to: txParamsToAddress } = txParams |
||||
const { |
||||
conversionRate, |
||||
identities, |
||||
currentCurrency, |
||||
accounts, |
||||
selectedAddress, |
||||
selectedAddressTxList, |
||||
} = metamask |
||||
|
||||
const { balance } = accounts[selectedAddress] |
||||
const { name: fromName } = identities[selectedAddress] |
||||
const toAddress = propsToAddress || txParamsToAddress |
||||
const toName = identities[toAddress] && identities[toAddress].name |
||||
const isTxReprice = Boolean(lastGasPrice) |
||||
|
||||
const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList) |
||||
const transactionStatus = transaction ? transaction.status : '' |
||||
|
||||
return { |
||||
balance, |
||||
fromAddress, |
||||
fromName, |
||||
toAddress, |
||||
toName, |
||||
ethTransactionAmount, |
||||
ethTransactionFee, |
||||
ethTransactionTotal, |
||||
fiatTransactionAmount, |
||||
fiatTransactionFee, |
||||
fiatTransactionTotal, |
||||
hexGasTotal, |
||||
txData, |
||||
tokenData, |
||||
methodData, |
||||
tokenProps, |
||||
isTxReprice, |
||||
currentCurrency, |
||||
conversionRate, |
||||
transactionStatus, |
||||
nonce, |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), |
||||
clearSend: () => dispatch(clearSend()), |
||||
showTransactionConfirmedModal: ({ onHide }) => { |
||||
return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide })) |
||||
}, |
||||
showCustomizeGasModal: ({ txData, onSubmit, validate }) => { |
||||
return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate })) |
||||
}, |
||||
updateGasAndCalculate: ({ gasLimit, gasPrice }) => { |
||||
return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) |
||||
}, |
||||
cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), |
||||
sendTransaction: txData => dispatch(updateAndApproveTx(txData)), |
||||
} |
||||
} |
||||
|
||||
const getValidateEditGas = ({ balance, conversionRate, txData }) => { |
||||
const { txParams: { value: amount } = {} } = txData |
||||
|
||||
return ({ gasLimit, gasPrice }) => { |
||||
const gasTotal = getHexGasTotal({ gasLimit, gasPrice }) |
||||
const hasSufficientBalance = isBalanceSufficient({ |
||||
amount, |
||||
gasTotal, |
||||
balance, |
||||
conversionRate, |
||||
}) |
||||
|
||||
if (!hasSufficientBalance) { |
||||
return { |
||||
valid: false, |
||||
errorKey: INSUFFICIENT_FUNDS_ERROR_KEY, |
||||
} |
||||
} |
||||
|
||||
const gasLimitTooLow = gasLimit && conversionGreaterThan( |
||||
{ |
||||
value: MIN_GAS_LIMIT_DEC, |
||||
fromNumericBase: 'dec', |
||||
conversionRate, |
||||
}, |
||||
{ |
||||
value: gasLimit, |
||||
fromNumericBase: 'hex', |
||||
}, |
||||
) |
||||
|
||||
if (gasLimitTooLow) { |
||||
return { |
||||
valid: false, |
||||
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY, |
||||
} |
||||
} |
||||
|
||||
return { |
||||
valid: true, |
||||
} |
||||
} |
||||
} |
||||
|
||||
const mergeProps = (stateProps, dispatchProps, ownProps) => { |
||||
const { balance, conversionRate, txData } = stateProps |
||||
const { |
||||
showCustomizeGasModal: dispatchShowCustomizeGasModal, |
||||
updateGasAndCalculate: dispatchUpdateGasAndCalculate, |
||||
...otherDispatchProps |
||||
} = dispatchProps |
||||
|
||||
const validateEditGas = getValidateEditGas({ balance, conversionRate, txData }) |
||||
|
||||
return { |
||||
...stateProps, |
||||
...otherDispatchProps, |
||||
...ownProps, |
||||
showCustomizeGasModal: () => dispatchShowCustomizeGasModal({ |
||||
txData, |
||||
onSubmit: txData => dispatchUpdateGasAndCalculate(txData), |
||||
validate: validateEditGas, |
||||
}), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps, mergeProps) |
||||
)(ConfirmTransactionBase) |
@ -0,0 +1 @@ |
||||
export { default } from './confirm-transaction-base.container' |
@ -0,0 +1,77 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { Redirect } from 'react-router-dom' |
||||
import Loading from '../../loading-screen' |
||||
import { |
||||
CONFIRM_TRANSACTION_ROUTE, |
||||
CONFIRM_DEPLOY_CONTRACT_PATH, |
||||
CONFIRM_SEND_ETHER_PATH, |
||||
CONFIRM_SEND_TOKEN_PATH, |
||||
CONFIRM_APPROVE_PATH, |
||||
CONFIRM_TOKEN_METHOD_PATH, |
||||
SIGNATURE_REQUEST_PATH, |
||||
} from '../../../routes' |
||||
import { isConfirmDeployContract } from './confirm-transaction-switch.util' |
||||
import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE } from './confirm-transaction-switch.constants' |
||||
|
||||
export default class ConfirmTransactionSwitch extends Component { |
||||
static propTypes = { |
||||
txData: PropTypes.object, |
||||
methodData: PropTypes.object, |
||||
fetchingMethodData: PropTypes.bool, |
||||
} |
||||
|
||||
redirectToTransaction () { |
||||
const { |
||||
txData, |
||||
methodData: { name }, |
||||
fetchingMethodData, |
||||
} = this.props |
||||
const { id } = txData |
||||
|
||||
|
||||
if (isConfirmDeployContract(txData)) { |
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}` |
||||
return <Redirect to={{ pathname }} /> |
||||
} |
||||
|
||||
if (fetchingMethodData) { |
||||
return <Loading /> |
||||
} |
||||
|
||||
if (name) { |
||||
const methodName = name.toLowerCase() |
||||
|
||||
switch (methodName.toLowerCase()) { |
||||
case TOKEN_METHOD_TRANSFER: { |
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}` |
||||
return <Redirect to={{ pathname }} /> |
||||
} |
||||
case TOKEN_METHOD_APPROVE: { |
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}` |
||||
return <Redirect to={{ pathname }} /> |
||||
} |
||||
default: { |
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}` |
||||
return <Redirect to={{ pathname }} /> |
||||
} |
||||
} |
||||
} |
||||
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}` |
||||
return <Redirect to={{ pathname }} /> |
||||
} |
||||
|
||||
render () { |
||||
const { txData } = this.props |
||||
|
||||
if (txData.txParams) { |
||||
return this.redirectToTransaction() |
||||
} else if (txData.msgParams) { |
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}` |
||||
return <Redirect to={{ pathname }} /> |
||||
} |
||||
|
||||
return <Loading /> |
||||
} |
||||
} |
@ -0,0 +1,2 @@ |
||||
export const TOKEN_METHOD_TRANSFER = 'transfer' |
||||
export const TOKEN_METHOD_APPROVE = 'approve' |
@ -0,0 +1,20 @@ |
||||
import { connect } from 'react-redux' |
||||
import ConfirmTransactionSwitch from './confirm-transaction-switch.component' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { |
||||
confirmTransaction: { |
||||
txData, |
||||
methodData, |
||||
fetchingMethodData, |
||||
}, |
||||
} = state |
||||
|
||||
return { |
||||
txData, |
||||
methodData, |
||||
fetchingMethodData, |
||||
} |
||||
} |
||||
|
||||
export default connect(mapStateToProps)(ConfirmTransactionSwitch) |
@ -0,0 +1,4 @@ |
||||
export function isConfirmDeployContract (txData = {}) { |
||||
const { txParams = {} } = txData |
||||
return !txParams.to |
||||
} |
@ -0,0 +1,2 @@ |
||||
import ConfirmTransactionSwitch from './confirm-transaction-switch.container' |
||||
module.exports = ConfirmTransactionSwitch |
@ -0,0 +1,150 @@ |
||||
import React, { Component } from 'react' |
||||
import PropTypes from 'prop-types' |
||||
import { Switch, Route } from 'react-router-dom' |
||||
import Loading from '../../loading-screen' |
||||
import ConfirmTransactionSwitch from '../confirm-transaction-switch' |
||||
import ConfirmTransactionBase from '../confirm-transaction-base' |
||||
import ConfirmSendEther from '../confirm-send-ether' |
||||
import ConfirmSendToken from '../confirm-send-token' |
||||
import ConfirmDeployContract from '../confirm-deploy-contract' |
||||
import ConfirmApprove from '../confirm-approve' |
||||
import ConfTx from '../../../conf-tx' |
||||
import { |
||||
DEFAULT_ROUTE, |
||||
CONFIRM_TRANSACTION_ROUTE, |
||||
CONFIRM_DEPLOY_CONTRACT_PATH, |
||||
CONFIRM_SEND_ETHER_PATH, |
||||
CONFIRM_SEND_TOKEN_PATH, |
||||
CONFIRM_APPROVE_PATH, |
||||
CONFIRM_TOKEN_METHOD_PATH, |
||||
SIGNATURE_REQUEST_PATH, |
||||
} from '../../../routes' |
||||
|
||||
export default class ConfirmTransaction extends Component { |
||||
static propTypes = { |
||||
history: PropTypes.object.isRequired, |
||||
totalUnapprovedCount: PropTypes.number.isRequired, |
||||
match: PropTypes.object, |
||||
send: PropTypes.object, |
||||
unconfirmedTransactions: PropTypes.array, |
||||
setTransactionToConfirm: PropTypes.func, |
||||
confirmTransaction: PropTypes.object, |
||||
clearConfirmTransaction: PropTypes.func, |
||||
} |
||||
|
||||
getParamsTransactionId () { |
||||
const { match: { params: { id } = {} } } = this.props |
||||
return id || null |
||||
} |
||||
|
||||
componentDidMount () { |
||||
const { |
||||
totalUnapprovedCount = 0, |
||||
send = {}, |
||||
history, |
||||
confirmTransaction: { txData: { id: transactionId } = {} }, |
||||
} = this.props |
||||
|
||||
if (!totalUnapprovedCount && !send.to) { |
||||
history.replace(DEFAULT_ROUTE) |
||||
return |
||||
} |
||||
|
||||
if (!transactionId) { |
||||
this.setTransactionToConfirm() |
||||
} |
||||
} |
||||
|
||||
componentDidUpdate () { |
||||
const { |
||||
setTransactionToConfirm, |
||||
confirmTransaction: { txData: { id: transactionId } = {} }, |
||||
clearConfirmTransaction, |
||||
} = this.props |
||||
const paramsTransactionId = this.getParamsTransactionId() |
||||
|
||||
if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') { |
||||
clearConfirmTransaction() |
||||
setTransactionToConfirm(paramsTransactionId) |
||||
return |
||||
} |
||||
|
||||
if (!transactionId) { |
||||
this.setTransactionToConfirm() |
||||
} |
||||
} |
||||
|
||||
setTransactionToConfirm () { |
||||
const { |
||||
history, |
||||
unconfirmedTransactions, |
||||
setTransactionToConfirm, |
||||
} = this.props |
||||
const paramsTransactionId = this.getParamsTransactionId() |
||||
|
||||
if (paramsTransactionId) { |
||||
// Check to make sure params ID is valid
|
||||
const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId) |
||||
|
||||
if (!tx) { |
||||
history.replace(DEFAULT_ROUTE) |
||||
} else { |
||||
setTransactionToConfirm(paramsTransactionId) |
||||
} |
||||
} else if (unconfirmedTransactions.length) { |
||||
const totalUnconfirmed = unconfirmedTransactions.length |
||||
const transaction = unconfirmedTransactions[totalUnconfirmed - 1] |
||||
const { id: transactionId, loadingDefaults } = transaction |
||||
|
||||
if (!loadingDefaults) { |
||||
setTransactionToConfirm(transactionId) |
||||
} |
||||
} |
||||
} |
||||
|
||||
render () { |
||||
const { confirmTransaction: { txData: { id } } = {} } = this.props |
||||
const paramsTransactionId = this.getParamsTransactionId() |
||||
|
||||
// Show routes when state.confirmTransaction has been set and when either the ID in the params
|
||||
// isn't specified or is specified and matches the ID in state.confirmTransaction in order to
|
||||
// support URLs of /confirm-transaction or /confirm-transaction/<transactionId>
|
||||
return id && (!paramsTransactionId || paramsTransactionId === id + '') |
||||
? ( |
||||
<Switch> |
||||
<Route |
||||
exact |
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`} |
||||
component={ConfirmDeployContract} |
||||
/> |
||||
<Route |
||||
exact |
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`} |
||||
component={ConfirmTransactionBase} |
||||
/> |
||||
<Route |
||||
exact |
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`} |
||||
component={ConfirmSendEther} |
||||
/> |
||||
<Route |
||||
exact |
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`} |
||||
component={ConfirmSendToken} |
||||
/> |
||||
<Route |
||||
exact |
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`} |
||||
component={ConfirmApprove} |
||||
/> |
||||
<Route |
||||
exact |
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} |
||||
component={ConfTx} |
||||
/> |
||||
<Route path="*" component={ConfirmTransactionSwitch} /> |
||||
</Switch> |
||||
) |
||||
: <Loading /> |
||||
} |
||||
} |
@ -0,0 +1,33 @@ |
||||
import { connect } from 'react-redux' |
||||
import { compose } from 'recompose' |
||||
import { withRouter } from 'react-router-dom' |
||||
import { |
||||
setTransactionToConfirm, |
||||
clearConfirmTransaction, |
||||
} from '../../../ducks/confirm-transaction.duck' |
||||
import ConfirmTransaction from './confirm-transaction.component' |
||||
import { getTotalUnapprovedCount } from '../../../selectors' |
||||
import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction' |
||||
|
||||
const mapStateToProps = state => { |
||||
const { metamask: { send }, confirmTransaction } = state |
||||
|
||||
return { |
||||
totalUnapprovedCount: getTotalUnapprovedCount(state), |
||||
send, |
||||
confirmTransaction, |
||||
unconfirmedTransactions: unconfirmedTransactionsListSelector(state), |
||||
} |
||||
} |
||||
|
||||
const mapDispatchToProps = dispatch => { |
||||
return { |
||||
setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)), |
||||
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), |
||||
} |
||||
} |
||||
|
||||
export default compose( |
||||
withRouter, |
||||
connect(mapStateToProps, mapDispatchToProps), |
||||
)(ConfirmTransaction) |
@ -0,0 +1,2 @@ |
||||
import ConfirmTransaction from './confirm-transaction.container' |
||||
module.exports = ConfirmTransaction |
@ -1,72 +0,0 @@ |
||||
const { Component } = require('react') |
||||
const h = require('react-hyperscript') |
||||
const connect = require('react-redux').connect |
||||
const PropTypes = require('prop-types') |
||||
const Identicon = require('./identicon') |
||||
|
||||
class SenderToRecipient extends Component { |
||||
renderRecipientIcon () { |
||||
const { recipientAddress } = this.props |
||||
return ( |
||||
recipientAddress |
||||
? h(Identicon, { address: recipientAddress, diameter: 20 }) |
||||
: h('i.fa.fa-file-text-o') |
||||
) |
||||
} |
||||
|
||||
renderRecipient () { |
||||
const { recipientName } = this.props |
||||
return ( |
||||
h('.sender-to-recipient__recipient', [ |
||||
this.renderRecipientIcon(), |
||||
h( |
||||
'.sender-to-recipient__name.sender-to-recipient__recipient-name', |
||||
recipientName || this.context.t('newContract') |
||||
), |
||||
]) |
||||
) |
||||
} |
||||
|
||||
render () { |
||||
const { senderName, senderAddress } = this.props |
||||
|
||||
return ( |
||||
h('.sender-to-recipient__container', [ |
||||
h('.sender-to-recipient__sender', [ |
||||
h('.sender-to-recipient__sender-icon', [ |
||||
h(Identicon, { |
||||
address: senderAddress, |
||||
diameter: 20, |
||||
}), |
||||
]), |
||||
h('.sender-to-recipient__name.sender-to-recipient__sender-name', senderName), |
||||
]), |
||||
h('.sender-to-recipient__arrow-container', [ |
||||
h('.sender-to-recipient__arrow-circle', [ |
||||
h('img', { |
||||
height: 15, |
||||
width: 15, |
||||
src: './images/arrow-right.svg', |
||||
}), |
||||
]), |
||||
]), |
||||
this.renderRecipient(), |
||||
]) |
||||
) |
||||
} |
||||
} |
||||
|
||||
SenderToRecipient.propTypes = { |
||||
senderName: PropTypes.string, |
||||
senderAddress: PropTypes.string, |
||||
recipientName: PropTypes.string, |
||||
recipientAddress: PropTypes.string, |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
SenderToRecipient.contextTypes = { |
||||
t: PropTypes.func, |
||||
} |
||||
|
||||
module.exports = connect()(SenderToRecipient) |
||||
|
@ -0,0 +1 @@ |
||||
export { default } from './sender-to-recipient.component' |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue