1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-22 17:33:23 +01:00

Refactor and redesign confirm transaction views

This commit is contained in:
Alexander Tseung 2018-06-22 23:52:45 -07:00
parent b4aaf30d6f
commit ea9d51e427
105 changed files with 6808 additions and 559 deletions

View File

@ -40,6 +40,9 @@
"message": "MetaMask", "message": "MetaMask",
"description": "The name of the application" "description": "The name of the application"
}, },
"approve": {
"message": "Approve"
},
"approved": { "approved": {
"message": "Approved" "message": "Approved"
}, },
@ -89,6 +92,9 @@
"buyCoinbaseExplainer": { "buyCoinbaseExplainer": {
"message": "Coinbase is the worlds most popular way to buy and sell bitcoin, ethereum, and litecoin." "message": "Coinbase is the worlds most popular way to buy and sell bitcoin, ethereum, and litecoin."
}, },
"bytes": {
"message": "Bytes"
},
"ok": { "ok": {
"message": "Ok" "message": "Ok"
}, },
@ -149,6 +155,9 @@
"copyContractAddress": { "copyContractAddress": {
"message": "Copy Contract Address" "message": "Copy Contract Address"
}, },
"copyAddress": {
"message": "Copy address to clipboard"
},
"copyToClipboard": { "copyToClipboard": {
"message": "Copy to clipboard" "message": "Copy to clipboard"
}, },
@ -318,6 +327,9 @@
"fromShapeShift": { "fromShapeShift": {
"message": "From ShapeShift" "message": "From ShapeShift"
}, },
"functionType": {
"message": "Function Type"
},
"gas": { "gas": {
"message": "Gas", "message": "Gas",
"description": "Short indication of gas cost" "description": "Short indication of gas cost"
@ -370,6 +382,9 @@
"hereList": { "hereList": {
"message": "Here's a list!!!!" "message": "Here's a list!!!!"
}, },
"hexData": {
"message": "Hex Data"
},
"hide": { "hide": {
"message": "Hide" "message": "Hide"
}, },
@ -582,6 +597,9 @@
"message": "or", "message": "or",
"description": "choice between creating or importing a new account" "description": "choice between creating or importing a new account"
}, },
"origin": {
"message": "Origin"
},
"password": { "password": {
"message": "Password" "message": "Password"
}, },
@ -911,6 +929,9 @@
"transactionNumber": { "transactionNumber": {
"message": "Transaction Number" "message": "Transaction Number"
}, },
"transfer": {
"message": "Transfer"
},
"transfers": { "transfers": {
"message": "Transfers" "message": "Transfers"
}, },

14
app/images/alert-red.svg Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>Artboard Copy</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Artboard-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-48">
<circle id="Oval" fill="#D0021B" cx="8" cy="8" r="8"></circle>
<rect id="Rectangle-41" fill="#FFFFFF" x="7" y="3" width="2" height="7" rx="1"></rect>
<rect id="Rectangle-41" fill="#FFFFFF" x="7" y="11" width="2" height="2" rx="1"></rect>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 774 B

19
app/images/alert.svg Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="29px" height="29px" viewBox="0 0 29 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>7414FFD8-B28A-4593-9D7E-19E73D687B50</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Approve---insufficient-amount" transform="translate(-69.000000, -166.000000)">
<g id="Group-7" transform="translate(53.000000, 51.000000)">
<g id="Group-34" transform="translate(0.000000, 91.000000)">
<g id="alert" transform="translate(16.000000, 24.000000)">
<circle id="Oval" fill="#605A1C" cx="14.5" cy="14.5" r="14.5"></circle>
<path d="M16,16.8282967 L14,16.8282967 L14,7 L16,7 L16,16.8282967 Z M16,21 L14,21 L14,19 L16,19 L16,21 Z" id="!" fill="#FFFCDB"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

18
app/images/caret-left.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="9px" height="15px" viewBox="0 0 9 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 50.2 (55047) - http://www.bohemiancoding.com/sketch -->
<title>8439120D-5704-4273-B416-FEE134322584</title>
<desc>Created with sketchtool.</desc>
<defs></defs>
<g id="Action-Screens" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Approve---insufficient-amount" transform="translate(-75.000000, -69.000000)" stroke="#3099F2" stroke-width="2">
<g id="Group-7" transform="translate(53.000000, 51.000000)">
<g id="cancel" transform="translate(24.000000, 14.000000)">
<g id="Group">
<polyline id="Path-8" points="6.1263881 18.0633906 0 11.6306831 6.31493631 5"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@ -4,6 +4,7 @@ const KOVAN = 'kovan'
const MAINNET = 'mainnet' const MAINNET = 'mainnet'
const LOCALHOST = 'localhost' const LOCALHOST = 'localhost'
const MAINNET_CODE = 1
const ROPSTEN_CODE = 3 const ROPSTEN_CODE = 3
const RINKEYBY_CODE = 4 const RINKEYBY_CODE = 4
const KOVAN_CODE = 42 const KOVAN_CODE = 42
@ -13,13 +14,13 @@ const RINKEBY_DISPLAY_NAME = 'Rinkeby'
const KOVAN_DISPLAY_NAME = 'Kovan' const KOVAN_DISPLAY_NAME = 'Kovan'
const MAINNET_DISPLAY_NAME = 'Main Ethereum Network' const MAINNET_DISPLAY_NAME = 'Main Ethereum Network'
module.exports = { module.exports = {
ROPSTEN, ROPSTEN,
RINKEBY, RINKEBY,
KOVAN, KOVAN,
MAINNET, MAINNET,
LOCALHOST, LOCALHOST,
MAINNET_CODE,
ROPSTEN_CODE, ROPSTEN_CODE,
RINKEYBY_CODE, RINKEYBY_CODE,
KOVAN_CODE, KOVAN_CODE,

View File

@ -399,19 +399,17 @@ class TransactionStateManager extends EventEmitter {
_setTxStatus (txId, status) { _setTxStatus (txId, status) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.status = status txMeta.status = status
setTimeout(() => { try {
try { this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
this.updateTx(txMeta, `txStateManager: setting status to ${status}`) this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`${txMeta.id}:${status}`, txId) this.emit(`tx:status-update`, txId, status)
this.emit(`tx:status-update`, txId, status) if (['submitted', 'rejected', 'failed'].includes(status)) {
if (['submitted', 'rejected', 'failed'].includes(status)) { this.emit(`${txMeta.id}:finished`, txMeta)
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.emit('update:badge')
} catch (error) {
log.error(error)
} }
}) this.emit('update:badge')
} catch (error) {
log.error(error)
}
} }
/** /**

3467
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -179,6 +179,7 @@
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0", "redux-thunk": "^2.2.0",
"request-promise": "^4.2.1", "request-promise": "^4.2.1",
"reselect": "^3.0.1",
"sandwich-expando": "^1.1.3", "sandwich-expando": "^1.1.3",
"semaphore": "^1.0.5", "semaphore": "^1.0.5",
"semver": "^5.4.1", "semver": "^5.4.1",

View File

@ -704,11 +704,10 @@ function signTypedMsg (msgData) {
function signTx (txData) { function signTx (txData) {
return (dispatch) => { return (dispatch) => {
dispatch(actions.showLoadingIndication())
global.ethQuery.sendTransaction(txData, (err, data) => { global.ethQuery.sendTransaction(txData, (err, data) => {
dispatch(actions.hideLoadingIndication()) if (err) {
if (err) return dispatch(actions.displayWarning(err.message)) return dispatch(actions.displayWarning(err.message))
dispatch(actions.hideWarning()) }
}) })
dispatch(actions.showConfTxPage({})) dispatch(actions.showConfTxPage({}))
} }
@ -910,29 +909,41 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) {
function updateTransaction (txData) { function updateTransaction (txData) {
log.info('actions: updateTx: ' + JSON.stringify(txData)) log.info('actions: updateTx: ' + JSON.stringify(txData))
return (dispatch) => { return dispatch => {
log.debug(`actions calling background.updateTx`) log.debug(`actions calling background.updateTx`)
background.updateTransaction(txData, (err) => { dispatch(actions.showLoadingIndication())
dispatch(actions.hideLoadingIndication())
dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) return new Promise((resolve, reject) => {
if (err) { background.updateTransaction(txData, (err) => {
dispatch(actions.txError(err)) dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
dispatch(actions.goHome()) if (err) {
return log.error(err.message) dispatch(actions.txError(err))
} dispatch(actions.goHome())
dispatch(actions.showConfTxPage({ id: txData.id })) log.error(err.message)
return reject(err)
}
resolve(txData)
})
}) })
.then(() => updateMetamaskStateFromBackground())
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => {
dispatch(actions.showConfTxPage({ id: txData.id }))
dispatch(actions.hideLoadingIndication())
return txData
})
} }
} }
function updateAndApproveTx (txData) { function updateAndApproveTx (txData) {
log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData)) log.info('actions: updateAndApproveTx: ' + JSON.stringify(txData))
return (dispatch) => { return dispatch => {
log.debug(`actions calling background.updateAndApproveTx`) log.debug(`actions calling background.updateAndApproveTx`)
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
background.updateAndApproveTransaction(txData, err => { background.updateAndApproveTransaction(txData, err => {
dispatch(actions.hideLoadingIndication())
dispatch(actions.updateTransactionParams(txData.id, txData.txParams)) dispatch(actions.updateTransactionParams(txData.id, txData.txParams))
dispatch(actions.clearSend()) dispatch(actions.clearSend())
@ -943,10 +954,17 @@ function updateAndApproveTx (txData) {
reject(err) reject(err)
} }
dispatch(actions.completedTx(txData.id))
resolve(txData) resolve(txData)
}) })
}) })
.then(() => updateMetamaskStateFromBackground())
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => {
dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
dispatch(actions.hideLoadingIndication())
return txData
})
} }
} }
@ -1038,13 +1056,25 @@ function cancelTypedMsg (msgData) {
function cancelTx (txData) { function cancelTx (txData) {
return dispatch => { return dispatch => {
log.debug(`background.cancelTransaction`) log.debug(`background.cancelTransaction`)
dispatch(actions.showLoadingIndication())
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
background.cancelTransaction(txData.id, () => { background.cancelTransaction(txData.id, err => {
dispatch(actions.clearSend()) if (err) {
dispatch(actions.completedTx(txData.id)) return reject(err)
resolve(txData) }
resolve()
}) })
}) })
.then(() => updateMetamaskStateFromBackground())
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => {
dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
dispatch(actions.hideLoadingIndication())
return txData
})
} }
} }

View File

@ -12,7 +12,7 @@ const log = require('loglevel')
const InitializeScreen = require('../../mascara/src/app/first-time').default const InitializeScreen = require('../../mascara/src/app/first-time').default
// accounts // accounts
const SendTransactionScreen = require('./components/send_/send.container') const SendTransactionScreen = require('./components/send_/send.container')
const ConfirmTxScreen = require('./conf-tx') const ConfirmTransaction = require('./components/pages/confirm-transaction')
// slideout menu // slideout menu
const WalletView = require('./components/wallet-view') const WalletView = require('./components/wallet-view')
@ -76,7 +76,10 @@ class App extends Component {
h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }), h(Authenticated, { path: REVEAL_SEED_ROUTE, exact, component: RevealSeedConfirmation }),
h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }), h(Authenticated, { path: SETTINGS_ROUTE, component: Settings }),
h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), h(Authenticated, { path: NOTICE_ROUTE, exact, component: NoticeScreen }),
h(Authenticated, { path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`, component: ConfirmTxScreen }), h(Authenticated, {
path: `${CONFIRM_TRANSACTION_ROUTE}/:id?`,
component: ConfirmTransaction,
}),
h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }),
h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }),
h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }), h(Authenticated, { path: CONFIRM_ADD_TOKEN_ROUTE, exact, component: ConfirmAddTokenPage }),

View File

@ -5,15 +5,24 @@ import classnames from 'classnames'
const CLASSNAME_DEFAULT = 'btn-default' const CLASSNAME_DEFAULT = 'btn-default'
const CLASSNAME_PRIMARY = 'btn-primary' const CLASSNAME_PRIMARY = 'btn-primary'
const CLASSNAME_SECONDARY = 'btn-secondary' const CLASSNAME_SECONDARY = 'btn-secondary'
const CLASSNAME_CONFIRM = 'btn-confirm'
const CLASSNAME_LARGE = 'btn--large' const CLASSNAME_LARGE = 'btn--large'
const typeHash = { const typeHash = {
default: CLASSNAME_DEFAULT, default: CLASSNAME_DEFAULT,
primary: CLASSNAME_PRIMARY, primary: CLASSNAME_PRIMARY,
secondary: CLASSNAME_SECONDARY, secondary: CLASSNAME_SECONDARY,
confirm: CLASSNAME_CONFIRM,
} }
class Button extends Component { export default class Button extends Component {
static propTypes = {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.string,
}
render () { render () {
const { type, large, className, ...buttonProps } = this.props const { type, large, className, ...buttonProps } = this.props
@ -31,13 +40,3 @@ class Button extends Component {
) )
} }
} }
Button.propTypes = {
type: PropTypes.string,
large: PropTypes.bool,
className: PropTypes.string,
children: PropTypes.string,
}
export default Button

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-detail-row.component'

View File

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

View File

@ -0,0 +1,100 @@
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,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
titleComponent: PropTypes.func,
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
hideSubtitle: PropTypes.bool,
errorMessage: PropTypes.string,
summaryComponent: PropTypes.node,
detailsComponent: PropTypes.node,
dataComponent: PropTypes.node,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
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,
title,
subtitle,
hideSubtitle,
errorMessage,
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() }
{
errorMessage && (
<div className="confirm-page-container-content__error-container">
<ConfirmPageContainerError error={errorMessage} />
</div>
)
}
</div>
)
}
}

View File

@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
const ConfirmPageContainerError = props => {
return (
<div className="confirm-page-container-error">
<img
src="/images/alert-red.svg"
className="confirm-page-container-error__icon"
/>
{ `ALERT: ${props.error}` }
</div>
)
}
ConfirmPageContainerError.propTypes = {
error: PropTypes.string,
}
export default ConfirmPageContainerError

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-error.component'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-summary.component'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-warning.component'

View File

@ -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: $oslo-gray;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-page-container-header.component'

View File

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

View File

@ -0,0 +1,116 @@
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 = {
showEdit: PropTypes.bool,
onEdit: PropTypes.func,
// Sender to Recipient
fromName: PropTypes.string,
fromAddress: PropTypes.string,
toName: PropTypes.string,
toAddress: PropTypes.string,
valid: PropTypes.bool,
errorMessage: PropTypes.string,
// Header
action: PropTypes.string,
title: PropTypes.string,
titleComponent: PropTypes.func,
subtitle: PropTypes.string,
hideSubtitle: PropTypes.bool,
// Content
summaryComponent: PropTypes.node,
contentComponent: PropTypes.node,
fiatTransactionAmount: PropTypes.string,
fiatTransactionFee: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
ethTransactionAmount: PropTypes.string,
ethTransactionFee: PropTypes.string,
ethTransactionTotal: PropTypes.string,
onEditGas: PropTypes.func,
detailsComponent: PropTypes.node,
dataComponent: PropTypes.node,
identiconAddress: PropTypes.string,
nonce: PropTypes.string,
warning: PropTypes.string,
// Footer
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
}
render () {
const {
showEdit,
onEdit,
fromName,
fromAddress,
toName,
toAddress,
valid,
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}
identiconAddress={identiconAddress}
nonce={nonce}
warning={warning}
/>
)
}
<PageContainerFooter
onCancel={() => onCancel()}
onSubmit={() => onSubmit()}
submitText={this.context.t('confirm')}
submitButtonType="confirm"
disabled={!valid}
/>
</div>
)
}
}

View File

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

View File

@ -0,0 +1,5 @@
@import './confirm-page-container-content/index';
@import './confirm-page-container-header/index';
@import './confirm-detail-row/index';

View File

@ -15,6 +15,7 @@ NetworkDropdownIcon.prototype.render = function () {
backgroundColor, backgroundColor,
isSelected, isSelected,
innerBorder = 'none', innerBorder = 'none',
diameter = '12',
} = this.props } = this.props
return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {}, return h(`.menu-icon-circle${isSelected ? '--active' : ''}`, {},
@ -22,6 +23,8 @@ NetworkDropdownIcon.prototype.render = function () {
style: { style: {
background: backgroundColor, background: backgroundColor,
border: innerBorder, border: innerBorder,
height: `${diameter}px`,
width: `${diameter}px`,
}, },
}) })
) )

View File

@ -4,6 +4,16 @@
@import './info-box/index'; @import './info-box/index';
@import './network-display/index';
@import './confirm-page-container/index';
@import './page-container/index';
@import './pages/index'; @import './pages/index';
@import './modals/index'; @import './modals/index';
@import './sender-to-recipient/index';
@import './tabs/index';

View File

@ -2,10 +2,8 @@ const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const inherits = require('util').inherits const inherits = require('util').inherits
const { const {
addCurrencies,
conversionGTE, conversionGTE,
conversionLTE, conversionLTE,
subtractCurrencies,
} = require('../conversion-util') } = require('../conversion-util')
module.exports = InputNumber module.exports = InputNumber
@ -51,7 +49,11 @@ InputNumber.prototype.setValue = function (newValue) {
} }
InputNumber.prototype.render = function () { InputNumber.prototype.render = function () {
<<<<<<< HEAD
const { unitLabel, step = 1, placeholder, value } = this.props const { unitLabel, step = 1, placeholder, value } = this.props
=======
const { unitLabel, step = 1, placeholder, value = 0, min = -1, max = Infinity } = this.props
>>>>>>> Refactor and redesign confirm transaction views
return h('div.customize-gas-input-wrapper', {}, [ return h('div.customize-gas-input-wrapper', {}, [
h('input', { h('input', {
@ -67,11 +69,19 @@ InputNumber.prototype.render = function () {
h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('span.gas-tooltip-input-detail', {}, [unitLabel]),
h('div.gas-tooltip-input-arrows', {}, [ h('div.gas-tooltip-input-arrows', {}, [
h('i.fa.fa-angle-up', { h('i.fa.fa-angle-up', {
<<<<<<< HEAD
onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })), onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })),
}), }),
h('i.fa.fa-angle-down', { h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' }, style: { cursor: 'pointer' },
onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })), onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })),
=======
onClick: () => this.setValue(Math.min(+value + step, max)),
}),
h('i.fa.fa-angle-down', {
style: { cursor: 'pointer' },
onClick: () => this.setValue(Math.max(+value - step, min)),
>>>>>>> Refactor and redesign confirm transaction views
}), }),
]), ]),
]) ])

View File

@ -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, errorMessage } = 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">{ errorMessage }</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>
)
}
}

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './customize-gas.container'

View File

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

View File

@ -1,3 +1,5 @@
@import './customize-gas/index';
.modal-container { .modal-container {
width: 100%; width: 100%;
height: 100%; height: 100%;

View File

@ -24,6 +24,8 @@ const TransactionConfirmed = require('./transaction-confirmed')
const WelcomeBeta = require('./welcome-beta') const WelcomeBeta = require('./welcome-beta')
const Notification = require('./notification') const Notification = require('./notification')
import ConfirmCustomizeGasModal from './customize-gas'
const modalContainerBaseStyle = { const modalContainerBaseStyle = {
transform: 'translate3d(-50%, 0, 0px)', transform: 'translate3d(-50%, 0, 0px)',
border: '1px solid #CCCFD1', border: '1px solid #CCCFD1',
@ -267,7 +269,31 @@ const MODALS = {
CUSTOMIZE_GAS: { CUSTOMIZE_GAS: {
contents: [ contents: [
h(CustomizeGasModal, {}, []), h(CustomizeGasModal),
],
mobileModalStyle: {
width: '100vw',
height: '100vh',
top: '0',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
laptopModalStyle: {
width: '720px',
height: '377px',
top: '80px',
transform: 'none',
left: '0',
right: '0',
margin: '0 auto',
},
},
CONFIRM_CUSTOMIZE_GAS: {
contents: [
h(ConfirmCustomizeGasModal),
], ],
mobileModalStyle: { mobileModalStyle: {
width: '100vw', width: '100vw',

View File

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

View File

@ -0,0 +1,2 @@
import NetworkDisplay from './network-display.container'
module.exports = NetworkDisplay

View File

@ -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: .875rem;
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;
}
}
}

View File

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

View File

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

View File

@ -1 +1,4 @@
import PageContainerHeader from './page-container-header'
import PageContainerFooter from './page-container-footer'
export { default } from './page-container.component' export { default } from './page-container.component'
export { PageContainerHeader, PageContainerFooter }

View File

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

View File

@ -10,6 +10,7 @@ export default class PageContainerFooter extends Component {
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
submitText: PropTypes.string, submitText: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
submitButtonType: PropTypes.string,
} }
static contextTypes = { static contextTypes = {
@ -23,6 +24,7 @@ export default class PageContainerFooter extends Component {
onSubmit, onSubmit,
submitText, submitText,
disabled, disabled,
submitButtonType,
} = this.props } = this.props
return ( return (
@ -30,16 +32,16 @@ export default class PageContainerFooter extends Component {
<Button <Button
type="default" type="default"
large={true} large
className="page-container__footer-button" className="page-container__footer-button"
onClick={() => onCancel()} onClick={e => onCancel(e)}
> >
{ cancelText || this.context.t('cancel') } { cancelText || this.context.t('cancel') }
</Button> </Button>
<Button <Button
type="primary" type={submitButtonType || 'primary'}
large={true} large
className="page-container__footer-button" className="page-container__footer-button"
disabled={disabled} disabled={disabled}
onClick={e => onSubmit(e)} onClick={e => onSubmit(e)}

View File

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

View File

@ -4,13 +4,14 @@ import PropTypes from 'prop-types'
export default class PageContainerHeader extends Component { export default class PageContainerHeader extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string.isRequired, title: PropTypes.string,
subtitle: PropTypes.string, subtitle: PropTypes.string,
onClose: PropTypes.func, onClose: PropTypes.func,
showBackButton: PropTypes.bool, showBackButton: PropTypes.bool,
onBackButtonClick: PropTypes.func, onBackButtonClick: PropTypes.func,
backButtonStyles: PropTypes.object, backButtonStyles: PropTypes.object,
backButtonString: PropTypes.string, backButtonString: PropTypes.string,
children: PropTypes.node,
}; };
renderHeaderRow () { renderHeaderRow () {
@ -30,25 +31,33 @@ export default class PageContainerHeader extends Component {
} }
render () { render () {
const { title, subtitle, onClose } = this.props const { title, subtitle, onClose, children } = this.props
return ( return (
<div className="page-container__header"> <div className="page-container__header">
{ this.renderHeaderRow() } { this.renderHeaderRow() }
<div className="page-container__title"> { children }
{title}
</div>
<div className="page-container__subtitle"> {
{subtitle} title && <div className="page-container__title">
</div> { title }
</div>
}
<div {
className="page-container__header-close" subtitle && <div className="page-container__subtitle">
onClick={() => onClose()} { subtitle }
/> </div>
}
{
onClose && <div
className="page-container__header-close"
onClick={() => onClose()}
/>
}
</div> </div>
) )

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-approve.container'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-deploy-contract.container'

View File

@ -0,0 +1,31 @@
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,
}
handleEdit ({ txData }) {
const { editTransaction, history } = this.props
editTransaction(txData)
history.push(SEND_ROUTE)
}
render () {
return (
<ConfirmTransactionBase
action={this.context.t('confirm')}
hideData
onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)}
/>
)
}
}

View File

@ -0,0 +1,37 @@
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 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(null, mapDispatchToProps)
)(ConfirmSendEther)

View File

@ -0,0 +1 @@
export { default } from './confirm-send-ether.container'

View File

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

View File

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

View File

@ -0,0 +1 @@
export { default } from './confirm-send-token.container'

View File

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

View File

@ -0,0 +1,382 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container'
import { formatCurrency, getHexGasTotal } from '../../../helpers/confirm-transaction/util'
import { isBalanceSufficient } from '../../send_/send.utils'
import { DEFAULT_ROUTE } from '../../../routes'
import { conversionGreaterThan } from '../../../conversion-util'
import { MIN_GAS_LIMIT_DEC } from '../../send_/send.constants'
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
t: PropTypes.func,
}
static propTypes = {
match: PropTypes.object,
history: PropTypes.object,
// Redux props
txData: PropTypes.object,
tokenData: PropTypes.object,
tokenProps: PropTypes.object,
isTxReprice: PropTypes.bool,
nonce: PropTypes.string,
fromName: PropTypes.string,
fromAddress: PropTypes.string,
toName: PropTypes.string,
toAddress: PropTypes.string,
transactionStatus: PropTypes.string,
ethTransactionAmount: PropTypes.string,
ethTransactionFee: PropTypes.string,
ethTransactionTotal: PropTypes.string,
fiatTransactionAmount: PropTypes.string,
fiatTransactionFee: PropTypes.string,
fiatTransactionTotal: PropTypes.string,
hexGasTotal: PropTypes.string,
balance: PropTypes.string,
currentCurrency: PropTypes.string,
conversionRate: PropTypes.number,
setTransactionToConfirm: PropTypes.func,
clearConfirmTransaction: PropTypes.func,
cancelTransaction: PropTypes.func,
clearSend: PropTypes.func,
sendTransaction: PropTypes.func,
editTransaction: PropTypes.func,
showCustomizeGasModal: PropTypes.func,
updateGasAndCalculate: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func,
// Component props
action: PropTypes.string,
hideDetails: PropTypes.bool,
hideData: PropTypes.bool,
detailsComponent: PropTypes.node,
dataComponent: PropTypes.node,
summaryComponent: PropTypes.node,
contentComponent: PropTypes.node,
title: PropTypes.string,
subtitle: PropTypes.string,
hideSubtitle: PropTypes.bool,
valid: PropTypes.bool,
errorMessage: PropTypes.string,
warning: PropTypes.string,
identiconAddress: PropTypes.string,
onEdit: PropTypes.func,
onEditGas: PropTypes.func,
onCancel: PropTypes.func,
onSubmit: PropTypes.func,
}
componentDidMount () {
const { match: { params: { id } = {} }, setTransactionToConfirm } = this.props
setTransactionToConfirm(id)
}
componentDidUpdate (prevProps) {
const {
transactionStatus,
showTransactionConfirmedModal,
history,
clearConfirmTransaction,
match: { params: { id } = {} },
setTransactionToConfirm,
txData,
} = this.props
if (transactionStatus === 'dropped') {
showTransactionConfirmedModal({
onHide: () => {
clearConfirmTransaction()
history.push(DEFAULT_ROUTE)
},
})
return
}
if (id && id !== txData.id + '') {
setTransactionToConfirm(id)
}
}
getError () {
const INSUFFICIENT_FUNDS_ERROR = this.context.t('insufficientFunds')
const TRANSACTION_ERROR = this.context.t('transactionError')
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,
errorMessage: INSUFFICIENT_FUNDS_ERROR,
}
}
if (simulationFails) {
return {
valid: false,
errorMessage: TRANSACTION_ERROR,
}
}
return {
valid: true,
}
}
validateEditGas ({ gasLimit, gasPrice }) {
const { t } = this.context
const {
balance,
conversionRate,
txData: {
txParams: {
value: amount,
} = {},
} = {},
} = this.props
const INSUFFICIENT_FUNDS_ERROR = t('insufficientFunds')
const GAS_LIMIT_TOO_LOW_ERROR = t('gasLimitTooLow')
const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
const hasSufficientBalance = isBalanceSufficient({
amount,
gasTotal,
balance,
conversionRate,
})
if (!hasSufficientBalance) {
return {
valid: false,
errorMessage: INSUFFICIENT_FUNDS_ERROR,
}
}
const gasLimitTooLow = gasLimit && conversionGreaterThan(
{
value: MIN_GAS_LIMIT_DEC,
fromNumericBase: 'dec',
conversionRate,
},
{
value: gasLimit,
fromNumericBase: 'hex',
},
)
if (gasLimitTooLow) {
return {
valid: false,
errorMessage: GAS_LIMIT_TOO_LOW_ERROR,
}
}
return {
valid: true,
}
}
handleEditGas () {
const { onEditGas, showCustomizeGasModal, txData, updateGasAndCalculate } = this.props
if (onEditGas) {
onEditGas()
} else {
showCustomizeGasModal({
txData,
onSubmit: txData => updateGasAndCalculate(txData),
validate: ({ gasLimit, gasPrice }) => this.validateEditGas({ gasLimit, gasPrice }),
})
}
}
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,
} = {},
} = {},
tokenData: {
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">
{ JSON.stringify(params, null, 2) }
</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,
tokenData,
ethTransactionAmount,
fiatTransactionAmount,
valid: propsValid,
errorMessage: propsErrorMessage,
currentCurrency,
action,
title,
subtitle,
hideSubtitle,
identiconAddress,
summaryComponent,
contentComponent,
onEdit,
nonce,
warning,
} = this.props
const { name } = tokenData
const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency)
const { valid, errorMessage } = this.getError()
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={propsErrorMessage || errorMessage}
warning={warning}
valid={propsValid || valid}
onEdit={() => this.handleEdit()}
onCancel={() => this.handleCancel()}
onSubmit={() => this.handleSubmit()}
/>
)
}
}

View File

@ -0,0 +1,95 @@
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 {
setTransactionToConfirm,
clearConfirmTransaction,
updateGasAndCalculate,
} from '../../../ducks/confirm-transaction.duck'
import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions'
const mapStateToProps = (state, props) => {
const { toAddress: propsToAddress } = props
const { confirmTransaction, metamask } = state
const {
ethTransactionAmount,
ethTransactionFee,
ethTransactionTotal,
fiatTransactionAmount,
fiatTransactionFee,
fiatTransactionTotal,
hexGasTotal,
tokenData,
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,
tokenProps,
isTxReprice,
currentCurrency,
conversionRate,
transactionStatus,
nonce,
}
}
const mapDispatchToProps = dispatch => {
return {
setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)),
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)),
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps)
)(ConfirmTransactionBase)

View File

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

View File

@ -0,0 +1,71 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Redirect } from 'react-router-dom'
import R from 'ramda'
import Loading from '../../loading-screen'
import {
CONFIRM_DEPLOY_CONTRACT_ROUTE,
CONFIRM_SEND_ETHER_ROUTE,
CONFIRM_SEND_TOKEN_ROUTE,
CONFIRM_APPROVE_ROUTE,
CONFIRM_TOKEN_METHOD_ROUTE,
SIGNATURE_REQUEST_ROUTE,
} from '../../../routes'
import { isConfirmDeployContract, getTokenData } 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 = {
unconfirmedTransactions: PropTypes.array,
match: PropTypes.object,
}
getTransaction () {
const { unconfirmedTransactions, match } = this.props
const { params: { id: paramsTransactionId } = {} } = match
return paramsTransactionId
? R.find(({ id }) => id + '' === paramsTransactionId)(unconfirmedTransactions)
: unconfirmedTransactions[0]
}
redirectToTransaction (txData) {
const { id, txParams: { data } } = txData
if (isConfirmDeployContract(txData)) {
return <Redirect to={{ pathname: `${CONFIRM_DEPLOY_CONTRACT_ROUTE}/${id}` }} />
}
if (data) {
const tokenData = getTokenData(data)
const { name: tokenMethodName } = tokenData || {}
switch (tokenMethodName) {
case TOKEN_METHOD_TRANSFER:
return <Redirect to={{ pathname: `${CONFIRM_SEND_TOKEN_ROUTE}/${id}` }} />
case TOKEN_METHOD_APPROVE:
return <Redirect to={{ pathname: `${CONFIRM_APPROVE_ROUTE}/${id}` }} />
default:
return <Redirect to={{ pathname: `${CONFIRM_TOKEN_METHOD_ROUTE}/${id}` }} />
}
}
return <Redirect to={{ pathname: `${CONFIRM_SEND_ETHER_ROUTE}/${id}` }} />
}
render () {
const txData = this.getTransaction()
if (!txData) {
return <Loading />
}
if (txData.txParams) {
return this.redirectToTransaction(txData)
} else if (txData.msgParams) {
return <Redirect to={{ pathname: SIGNATURE_REQUEST_ROUTE }} />
}
return <Loading />
}
}

View File

@ -0,0 +1,2 @@
export const TOKEN_METHOD_TRANSFER = 'transfer'
export const TOKEN_METHOD_APPROVE = 'approve'

View File

@ -0,0 +1,11 @@
import { connect } from 'react-redux'
import ConfirmTransactionSwitch from './confirm-transaction-switch.component'
import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction'
const mapStateToProps = state => {
return {
unconfirmedTransactions: unconfirmedTransactionsListSelector(state),
}
}
export default connect(mapStateToProps)(ConfirmTransactionSwitch)

View File

@ -0,0 +1,12 @@
import abi from 'human-standard-token-abi'
import abiDecoder from 'abi-decoder'
abiDecoder.addABI(abi)
export function isConfirmDeployContract (txData = {}) {
const { txParams = {} } = txData
return !txParams.to
}
export function getTokenData (data = {}) {
return abiDecoder.decodeMethod(data)
}

View File

@ -0,0 +1,2 @@
import ConfirmTransactionSwitch from './confirm-transaction-switch.container'
module.exports = ConfirmTransactionSwitch

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Switch, Route } from 'react-router-dom'
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_ROUTE,
CONFIRM_SEND_ETHER_ROUTE,
CONFIRM_SEND_TOKEN_ROUTE,
CONFIRM_APPROVE_ROUTE,
CONFIRM_TOKEN_METHOD_ROUTE,
SIGNATURE_REQUEST_ROUTE,
} from '../../../routes'
export default class ConfirmTransaction extends Component {
static propTypes = {
history: PropTypes.object.isRequired,
totalUnapprovedCount: PropTypes.number.isRequired,
match: PropTypes.object,
send: PropTypes.object,
}
componentDidMount () {
const { totalUnapprovedCount = 0, send = {}, history } = this.props
if (!totalUnapprovedCount && !send.to) {
history.replace(DEFAULT_ROUTE)
}
}
render () {
return (
<Switch>
<Route
exact
path={`${CONFIRM_DEPLOY_CONTRACT_ROUTE}/:id?`}
component={ConfirmDeployContract}
/>
<Route
exact
path={`${CONFIRM_TOKEN_METHOD_ROUTE}/:id?`}
component={ConfirmTransactionBase}
/>
<Route exact path={`${CONFIRM_SEND_ETHER_ROUTE}/:id?`} component={ConfirmSendEther} />
<Route exact path={`${CONFIRM_SEND_TOKEN_ROUTE}/:id?`} component={ConfirmSendToken} />
<Route exact path={`${CONFIRM_APPROVE_ROUTE}/:id?`} component={ConfirmApprove} />
<Route exact path={SIGNATURE_REQUEST_ROUTE} component={ConfTx} />
<Route path={`${CONFIRM_TRANSACTION_ROUTE}/:id?`} component={ConfirmTransactionSwitch} />
</Switch>
)
}
}

View File

@ -0,0 +1,19 @@
import { connect } from 'react-redux'
import { compose } from 'recompose'
import { withRouter } from 'react-router-dom'
import ConfirmTransaction from './confirm-transaction.component'
import { getTotalUnapprovedCount } from '../../../selectors'
const mapStateToProps = (state, props) => {
const { metamask: { send } } = state
return {
totalUnapprovedCount: getTotalUnapprovedCount(state),
send,
}
}
export default compose(
withRouter,
connect(mapStateToProps),
)(ConfirmTransaction)

View File

@ -0,0 +1,2 @@
import ConfirmTransaction from './confirm-transaction.container'
module.exports = ConfirmTransaction

View File

@ -83,51 +83,6 @@ class Home extends Component {
}) })
} }
// if (!props.noActiveNotices) {
// log.debug('rendering notice screen for unread notices.')
// return h(NoticeScreen, {
// notice: props.nextUnreadNotice,
// key: 'NoticeScreen',
// onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)),
// })
// } else if (props.lostAccounts && props.lostAccounts.length > 0) {
// log.debug('rendering notice screen for lost accounts view.')
// return h(NoticeScreen, {
// notice: generateLostAccountsNotice(props.lostAccounts),
// key: 'LostAccountsNotice',
// onConfirm: () => props.dispatch(actions.markAccountsFound()),
// })
// }
// if (props.seedWords) {
// log.debug('rendering seed words')
// return h(HDCreateVaultComplete, {key: 'HDCreateVaultComplete'})
// }
// show initialize screen
// if (!isInitialized || forgottenPassword) {
// // show current view
// log.debug('rendering an initialize screen')
// // switch (props.currentView.name) {
// // case 'restoreVault':
// // log.debug('rendering restore vault screen')
// // return h(HDRestoreVaultScreen, {key: 'HDRestoreVaultScreen'})
// // default:
// // log.debug('rendering menu screen')
// // return h(InitializeScreen, {key: 'menuScreenInit'})
// // }
// }
// // show unlock screen
// if (!props.isUnlocked) {
// return h(MainContainer, {
// currentViewName: props.currentView.name,
// isUnlocked: props.isUnlocked,
// })
// }
// show current view // show current view
switch (currentView.name) { switch (currentView.name) {
@ -135,59 +90,10 @@ class Home extends Component {
log.debug('rendering main container') log.debug('rendering main container')
return h(MainContainer, {key: 'account-detail'}) return h(MainContainer, {key: 'account-detail'})
// case 'sendTransaction':
// log.debug('rendering send tx screen')
// // Going to leave this here until we are ready to delete SendTransactionScreen v1
// // const SendComponentToRender = checkFeatureToggle('send-v2')
// // ? SendTransactionScreen2
// // : SendTransactionScreen
// return h(SendTransactionScreen2, {key: 'send-transaction'})
// case 'sendToken':
// log.debug('rendering send token screen')
// // Going to leave this here until we are ready to delete SendTransactionScreen v1
// // const SendTokenComponentToRender = checkFeatureToggle('send-v2')
// // ? SendTransactionScreen2
// // : SendTokenScreen
// return h(SendTransactionScreen2, {key: 'sendToken'})
case 'newKeychain': case 'newKeychain':
log.debug('rendering new keychain screen') log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'}) return h(NewKeyChainScreen, {key: 'new-keychain'})
// case 'confTx':
// log.debug('rendering confirm tx screen')
// return h(Redirect, {
// to: {
// pathname: CONFIRM_TRANSACTION_ROUTE,
// },
// })
// return h(ConfirmTxScreen, {key: 'confirm-tx'})
// case 'add-token':
// log.debug('rendering add-token screen from unlock screen.')
// return h(AddTokenScreen, {key: 'add-token'})
// case 'config':
// log.debug('rendering config screen')
// return h(Settings, {key: 'config'})
// case 'import-menu':
// log.debug('rendering import screen')
// return h(Import, {key: 'import-menu'})
// case 'reveal-seed-conf':
// log.debug('rendering reveal seed confirmation screen')
// return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'})
// case 'info':
// log.debug('rendering info screen')
// return h(Settings, {key: 'info', tab: 'info'})
case 'buyEth': case 'buyEth':
log.debug('rendering buy ether screen') log.debug('rendering buy ether screen')
return h(BuyView, {key: 'buyEthView'}) return h(BuyView, {key: 'buyEthView'})

View File

@ -3,3 +3,5 @@
@import './add-token/index'; @import './add-token/index';
@import './confirm-add-token/index'; @import './confirm-add-token/index';
@import './confirm-send-token/index';

View File

@ -48,6 +48,7 @@ export default class SendFooter extends Component {
// updateTx, // updateTx,
update, update,
toAccounts, toAccounts,
history,
} = this.props } = this.props
// Should not be needed because submit should be disabled if there are errors. // Should not be needed because submit should be disabled if there are errors.
@ -60,7 +61,7 @@ export default class SendFooter extends Component {
// TODO: add nickname functionality // TODO: add nickname functionality
addToAddressBookIfNew(to, toAccounts) addToAddressBookIfNew(to, toAccounts)
editingTransactionId const promise = editingTransactionId
? update({ ? update({
amount, amount,
editingTransactionId, editingTransactionId,
@ -73,7 +74,8 @@ export default class SendFooter extends Component {
}) })
: sign({ selectedToken, to, amount, from, gas, gasPrice }) : sign({ selectedToken, to, amount, from, gas, gasPrice })
this.props.history.push(CONFIRM_TRANSACTION_ROUTE) Promise.resolve(promise)
.then(() => history.push(CONFIRM_TRANSACTION_ROUTE))
} }
formShouldBeDisabled () { formShouldBeDisabled () {

View File

@ -87,7 +87,7 @@ function mapDispatchToProps (dispatch) {
unapprovedTxs, unapprovedTxs,
}) })
dispatch(updateTransaction(editingTx)) return dispatch(updateTransaction(editingTx))
}, },
addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => { addToAddressBookIfNew: (newAddress, toAccounts, nickname = '') => {
const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress) const hexPrefixedAddress = ethUtil.addHexPrefix(newAddress)

View File

@ -37,7 +37,7 @@ module.exports = {
removeLeadingZeroes, removeLeadingZeroes,
} }
function calcGasTotal (gasLimit, gasPrice) { function calcGasTotal (gasLimit = '0', gasPrice = '0') {
return multiplyCurrencies(gasLimit, gasPrice, { return multiplyCurrencies(gasLimit, gasPrice, {
toNumericBase: 'hex', toNumericBase: 'hex',
multiplicandBase: 16, multiplicandBase: 16,
@ -47,9 +47,9 @@ function calcGasTotal (gasLimit, gasPrice) {
function isBalanceSufficient ({ function isBalanceSufficient ({
amount = '0x0', amount = '0x0',
amountConversionRate = 0, amountConversionRate = 1,
balance, balance = '0x0',
conversionRate, conversionRate = 1,
gasTotal = '0x0', gasTotal = '0x0',
primaryCurrency, primaryCurrency,
}) { }) {

View File

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

View File

@ -0,0 +1 @@
export { default } from './sender-to-recipient.component'

View File

@ -6,6 +6,16 @@
justify-content: center; justify-content: center;
border-bottom: 1px solid $geyser; border-bottom: 1px solid $geyser;
position: relative; position: relative;
flex: 0 0 auto;
height: 42px;
}
&__tooltip-wrapper {
min-width: 0;
}
&__tooltip-container {
max-width: 100%;
} }
&__sender, &__sender,
@ -14,7 +24,7 @@
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
flex: 1; flex: 1;
padding: 10px 20px; padding: 0 16px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -22,11 +32,16 @@
&__sender { &__sender {
padding-right: 30px; padding-right: 30px;
cursor: pointer;
} }
&__recipient { &__recipient {
border-left: 1px solid $geyser;
padding-left: 30px; padding-left: 30px;
border-left: 1px solid $geyser;
&--with-address {
cursor: pointer;
}
} }
&__arrow-container { &__arrow-container {
@ -42,17 +57,18 @@
padding: 5px; padding: 5px;
border: 1px solid $geyser; border: 1px solid $geyser;
border-radius: 20px; border-radius: 20px;
height: 30px; height: 32px;
width: 30px; width: 32px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
&__name { &__name {
padding-left: 5px; padding-left: 14px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-size: .875rem;
} }
} }

View File

@ -0,0 +1,117 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Identicon from '../identicon'
import Tooltip from '../tooltip-v2'
import copyToClipboard from 'copy-to-clipboard'
export default class SenderToRecipient extends Component {
static propTypes = {
senderName: PropTypes.string,
senderAddress: PropTypes.string,
recipientName: PropTypes.string,
recipientAddress: PropTypes.string,
t: PropTypes.func,
}
static contextTypes = {
t: PropTypes.func,
}
state = {
senderAddressCopied: false,
recipientAddressCopied: false,
}
renderRecipientWithAddress () {
const { t } = this.context
const { recipientName, recipientAddress } = this.props
return (
<div
className="sender-to-recipient__recipient sender-to-recipient__recipient--with-address"
onClick={() => {
this.setState({ recipientAddressCopied: true })
copyToClipboard(recipientAddress)
}}
>
<div className="sender-to-recipient__sender-icon">
<Identicon
address={recipientAddress}
diameter={24}
/>
</div>
<Tooltip
position="bottom"
title={this.state.recipientAddressCopied ? t('copiedExclamation') : t('copyAddress')}
wrapperClassName="sender-to-recipient__tooltip-wrapper"
containerClassName="sender-to-recipient__tooltip-container"
onHidden={() => this.setState({ recipientAddressCopied: false })}
>
<div className="sender-to-recipient__name sender-to-recipient__recipient-name">
{ recipientName || this.context.t('newContract') }
</div>
</Tooltip>
</div>
)
}
renderRecipientWithoutAddress () {
return (
<div className="sender-to-recipient__recipient">
<i className="fa fa-file-text-o" />
<div className="sender-to-recipient__name sender-to-recipient__recipient-name">
{ this.context.t('newContract') }
</div>
</div>
)
}
render () {
const { t } = this.context
const { senderName, senderAddress, recipientAddress } = this.props
return (
<div className="sender-to-recipient__container">
<div
className="sender-to-recipient__sender"
onClick={() => {
this.setState({ senderAddressCopied: true })
copyToClipboard(senderAddress)
}}
>
<div className="sender-to-recipient__sender-icon">
<Identicon
address={senderAddress}
diameter={24}
/>
</div>
<Tooltip
position="bottom"
title={this.state.senderAddressCopied ? t('copiedExclamation') : t('copyAddress')}
wrapperClassName="sender-to-recipient__tooltip-wrapper"
containerClassName="sender-to-recipient__tooltip-container"
onHidden={() => this.setState({ senderAddressCopied: false })}
>
<div className="sender-to-recipient__name sender-to-recipient__sender-name">
{ senderName }
</div>
</Tooltip>
</div>
<div className="sender-to-recipient__arrow-container">
<div className="sender-to-recipient__arrow-circle">
<img
height={15}
width={15}
src="./images/arrow-right.svg"
/>
</div>
</div>
{
recipientAddress
? this.renderRecipientWithAddress()
: this.renderRecipientWithoutAddress()
}
</div>
)
}
}

View File

@ -0,0 +1,3 @@
import Tabs from './tabs.component'
import Tab from './tab'
export { Tabs, Tab }

View File

@ -0,0 +1,11 @@
@import './tab/index';
.tabs {
&__list {
display: flex;
justify-content: flex-start;
background-color: #f9fafa;
border-bottom: 1px solid $geyser;
padding: 0 16px;
}
}

View File

@ -0,0 +1,2 @@
import Tab from './tab.component'
module.exports = Tab

View File

@ -0,0 +1,15 @@
.tab {
color: #8C8E94;
font-size: .75rem;
text-transform: uppercase;
cursor: pointer;
padding: 8px 0;
margin: 0 8px;
min-width: 50px;
text-align: center;
&--active {
color: $black;
border-bottom: 2px solid $curious-blue;
}
}

View File

@ -0,0 +1,31 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
const Tab = props => {
const { name, onClick, isActive, tabIndex } = props
return (
<li
className={classnames(
'tab',
isActive && 'tab--active',
)}
onClick={event => {
event.preventDefault()
onClick(tabIndex)
}}
>
{ name }
</li>
)
}
Tab.propTypes = {
name: PropTypes.string.isRequired,
onClick: PropTypes.func,
isActive: PropTypes.bool,
tabIndex: PropTypes.number,
}
export default Tab

View File

@ -0,0 +1,63 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Tabs extends Component {
static propTypes = {
defaultActiveTabIndex: PropTypes.number,
children: PropTypes.node,
}
constructor (props) {
super(props)
this.state = {
activeTabIndex: props.defaultActiveTabIndex || 0,
}
}
handleTabClick (tabIndex) {
const { activeTabIndex } = this.state
if (tabIndex !== activeTabIndex) {
this.setState({
activeTabIndex: tabIndex,
})
}
}
renderTabs () {
// const { children } = this.props
const numberOfTabs = React.Children.count(this.props.children)
return React.Children.map(this.props.children, (child, index) => {
return child && React.cloneElement(child, {
onClick: index => this.handleTabClick(index),
tabIndex: index,
isActive: numberOfTabs > 1 && index === this.state.activeTabIndex,
key: index,
})
})
}
renderActiveTabContent () {
const { children } = this.props
const { activeTabIndex } = this.state
return children[activeTabIndex]
? children[activeTabIndex].props.children
: children.props.children
}
render () {
return (
<div className="tabs">
<ul className="tabs__list">
{ this.renderTabs() }
</ul>
<div className="tabs__content">
{ this.renderActiveTabContent() }
</div>
</div>
)
}
}

View File

@ -12,7 +12,7 @@ function Tooltip () {
Tooltip.prototype.render = function () { Tooltip.prototype.render = function () {
const props = this.props const props = this.props
const { position, title, children, wrapperClassName } = props const { position, title, children, wrapperClassName, containerClassName, onHidden } = props
return h('div', { return h('div', {
className: wrapperClassName, className: wrapperClassName,
@ -25,6 +25,8 @@ Tooltip.prototype.render = function () {
hideOnClick: false, hideOnClick: false,
size: 'small', size: 'small',
arrow: true, arrow: true,
className: containerClassName,
onHidden,
}, children), }, children),
]) ])

View File

@ -28,6 +28,8 @@ const BN = ethUtil.BN
const R = require('ramda') const R = require('ramda')
const { stripHexPrefix } = require('ethereumjs-util') const { stripHexPrefix } = require('ethereumjs-util')
global.BigNumber = BigNumber
BigNumber.config({ BigNumber.config({
ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN, ROUNDING_MODE: BigNumber.ROUND_HALF_DOWN,
}) })

View File

@ -4,7 +4,8 @@
.btn-default, .btn-default,
.btn-primary, .btn-primary,
.btn-secondary { .btn-secondary,
.btn-confirm {
height: 44px; height: 44px;
background: $white; background: $white;
display: flex; display: flex;
@ -13,13 +14,14 @@
box-sizing: border-box; box-sizing: border-box;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 400;
transition: border-color .3s ease; transition: border-color .3s ease;
padding: 0 16px; padding: 0 16px;
min-width: 140px; min-width: 140px;
width: 100%; width: 100%;
text-transform: uppercase; text-transform: uppercase;
outline: none; outline: none;
font-family: Roboto;
&--disabled, &--disabled,
&[disabled] { &[disabled] {
@ -71,6 +73,12 @@
} }
} }
.btn-confirm {
color: $white;
border: 2px solid $curious-blue;
background-color: $curious-blue;
}
.btn--large { .btn--large {
height: 54px; height: 54px;
} }
@ -119,19 +127,6 @@
} }
} }
.btn-confirm {
background-color: $caribbean-green; // TODO: reusable color in colors.css
text-align: center;
padding: .8rem 1rem;
color: $white;
border: 2px solid $caribbean-green;
border-radius: 4px;
font-size: .85rem;
font-weight: 400;
transition: border-color .3s ease;
width: 100%;
}
// No longer used in flat design, remove when modal buttons done // No longer used in flat design, remove when modal buttons done
// div.wallet-btn { // div.wallet-btn {
// border: 1px solid rgb(91, 93, 103); // border: 1px solid rgb(91, 93, 103);

View File

@ -58,6 +58,4 @@
@import './welcome-screen.scss'; @import './welcome-screen.scss';
@import './sender-to-recipient.scss';
@import '../../../components/index'; @import '../../../components/index';

View File

@ -159,15 +159,3 @@
.network-caret { .network-caret {
margin: 0 8px 2px; margin: 0 8px 2px;
} }
.network-display {
&__container {
display: flex;
align-items: center;
justify-content: flex-start;
@media screen and (min-width: 576px) {
display: none;
}
}
}

View File

@ -73,195 +73,6 @@ input.large-input {
text-transform: uppercase; text-transform: uppercase;
} }
.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-clear,
.btn-cancel,
.btn-confirm {
font-size: 1rem;
}
}
&__footer-button {
height: 55px;
font-size: 1rem;
text-transform: uppercase;
margin-right: 16px;
border-radius: 2px;
&: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;
}
}
.input-label { .input-label {
padding-bottom: 10px; padding-bottom: 10px;
font-weight: 400; font-weight: 400;

View File

@ -55,6 +55,7 @@ $dodger-blue: #3099f2;
$zumthor: #edf7ff; $zumthor: #edf7ff;
$ecstasy: #f7861c; $ecstasy: #f7861c;
$linen: #fdf4f4; $linen: #fdf4f4;
$oslo-gray: #8C8E94;
/* /*
Z-Indicies Z-Indicies

View File

@ -165,7 +165,7 @@
} }
.bold { .bold {
font-weight: 700; font-weight: 500;
} }
.text-transform-uppercase { .text-transform-uppercase {

View File

@ -0,0 +1,319 @@
import {
conversionRateSelector,
currentCurrencySelector,
unconfirmedTransactionsHashSelector,
} from '../selectors/confirm-transaction'
import {
getTokenData,
getTransactionAmount,
getTransactionFee,
getHexGasTotal,
addFiat,
addEth,
increaseLastGasPrice,
hexGreaterThan,
} from '../helpers/confirm-transaction/util'
import { getSymbolAndDecimals } from '../token-util'
import { conversionUtil } from '../conversion-util'
// Actions
const createActionType = action => `metamask/confirm-transaction/${action}`
const UPDATE_TX_DATA = createActionType('UPDATE_TX_DATA')
const CLEAR_TX_DATA = createActionType('CLEAR_TX_DATA')
const UPDATE_TOKEN_DATA = createActionType('UPDATE_TOKEN_DATA')
const CLEAR_TOKEN_DATA = createActionType('CLEAR_TOKEN_DATA')
const CLEAR_CONFIRM_TRANSACTION = createActionType('CLEAR_CONFIRM_TRANSACTION')
const UPDATE_TRANSACTION_AMOUNTS = createActionType('UPDATE_TRANSACTION_AMOUNTS')
const UPDATE_TRANSACTION_FEES = createActionType('UPDATE_TRANSACTION_FEES')
const UPDATE_TRANSACTION_TOTALS = createActionType('UPDATE_TRANSACTION_TOTALS')
const UPDATE_HEX_GAS_TOTAL = createActionType('UPDATE_HEX_GAS_TOTAL')
const UPDATE_TOKEN_PROPS = createActionType('UPDATE_TOKEN_PROPS')
const UPDATE_NONCE = createActionType('UPDATE_NONCE')
// Initial state
const initState = {
txData: {},
tokenData: {},
tokenProps: {
tokenDecimals: '',
tokenSymbol: '',
},
fiatTransactionAmount: '',
fiatTransactionFee: '',
fiatTransactionTotal: '',
ethTransactionAmount: '',
ethTransactionFee: '',
ethTransactionTotal: '',
hexGasTotal: '',
nonce: '',
}
// Reducer
export default function reducer ({ confirmTransaction: confirmState = initState }, action = {}) {
switch (action.type) {
case UPDATE_TX_DATA:
return {
...confirmState,
txData: {
...action.payload,
},
}
case CLEAR_TX_DATA:
return {
...confirmState,
txData: {},
}
case UPDATE_TOKEN_DATA:
return {
...confirmState,
tokenData: {
...action.payload,
},
}
case CLEAR_TOKEN_DATA:
return {
...confirmState,
tokenData: {},
}
case UPDATE_TRANSACTION_AMOUNTS:
const { fiatTransactionAmount, ethTransactionAmount } = action.payload
return {
...confirmState,
fiatTransactionAmount: fiatTransactionAmount || confirmState.fiatTransactionAmount,
ethTransactionAmount: ethTransactionAmount || confirmState.ethTransactionAmount,
}
case UPDATE_TRANSACTION_FEES:
const { fiatTransactionFee, ethTransactionFee } = action.payload
return {
...confirmState,
fiatTransactionFee: fiatTransactionFee || confirmState.fiatTransactionFee,
ethTransactionFee: ethTransactionFee || confirmState.ethTransactionFee,
}
case UPDATE_TRANSACTION_TOTALS:
const { fiatTransactionTotal, ethTransactionTotal } = action.payload
return {
...confirmState,
fiatTransactionTotal: fiatTransactionTotal || confirmState.fiatTransactionTotal,
ethTransactionTotal: ethTransactionTotal || confirmState.ethTransactionTotal,
}
case UPDATE_HEX_GAS_TOTAL:
return {
...confirmState,
hexGasTotal: action.payload,
}
case UPDATE_TOKEN_PROPS:
const { tokenSymbol = '', tokenDecimals = '' } = action.payload
return {
...confirmState,
tokenProps: {
...confirmState.tokenProps,
tokenSymbol,
tokenDecimals,
},
}
case UPDATE_NONCE:
return {
...confirmState,
nonce: action.payload,
}
case CLEAR_CONFIRM_TRANSACTION:
return initState
default:
return confirmState
}
}
// Action Creators
export function updateTxData (txData) {
return {
type: UPDATE_TX_DATA,
payload: txData,
}
}
export function clearTxData () {
return {
type: CLEAR_TX_DATA,
}
}
export function updateTokenData (tokenData) {
return {
type: UPDATE_TOKEN_DATA,
payload: tokenData,
}
}
export function clearTokenData () {
return {
type: CLEAR_TOKEN_DATA,
}
}
export function updateTransactionAmounts (amounts) {
return {
type: UPDATE_TRANSACTION_AMOUNTS,
payload: amounts,
}
}
export function updateTransactionFees (fees) {
return {
type: UPDATE_TRANSACTION_FEES,
payload: fees,
}
}
export function updateTransactionTotals (totals) {
return {
type: UPDATE_TRANSACTION_TOTALS,
payload: totals,
}
}
export function updateHexGasTotal (hexGasTotal) {
return {
type: UPDATE_HEX_GAS_TOTAL,
payload: hexGasTotal,
}
}
export function updateTokenProps (tokenProps) {
return {
type: UPDATE_TOKEN_PROPS,
payload: tokenProps,
}
}
export function updateNonce (nonce) {
return {
type: UPDATE_NONCE,
payload: nonce,
}
}
export function updateGasAndCalculate ({ gasLimit, gasPrice }) {
return (dispatch, getState) => {
const { confirmTransaction: { txData } } = getState()
const newTxData = {
...txData,
txParams: {
...txData.txParams,
gas: gasLimit,
gasPrice,
},
}
dispatch(updateTxDataAndCalculate(newTxData))
}
}
function increaseFromLastGasPrice (txData) {
const { lastGasPrice, txParams: { gasPrice: previousGasPrice } = {} } = txData
// Set the minimum to a 10% increase from the lastGasPrice.
const minimumGasPrice = increaseLastGasPrice(lastGasPrice)
const gasPriceBelowMinimum = hexGreaterThan(minimumGasPrice, previousGasPrice)
const gasPrice = (!previousGasPrice || gasPriceBelowMinimum) ? minimumGasPrice : previousGasPrice
return {
...txData,
txParams: {
...txData.txParams,
gasPrice,
},
}
}
export function updateTxDataAndCalculate (txData) {
return (dispatch, getState) => {
const state = getState()
const currentCurrency = currentCurrencySelector(state)
const conversionRate = conversionRateSelector(state)
dispatch(updateTxData(txData))
const { txParams: { value, gas: gasLimit, gasPrice } = {} } = txData
const fiatTransactionAmount = getTransactionAmount({
value, toCurrency: currentCurrency, conversionRate, numberOfDecimals: 2,
})
const ethTransactionAmount = getTransactionAmount({
value, toCurrency: 'ETH', conversionRate, numberOfDecimals: 6,
})
dispatch(updateTransactionAmounts({ fiatTransactionAmount, ethTransactionAmount }))
const hexGasTotal = getHexGasTotal({ gasLimit, gasPrice })
dispatch(updateHexGasTotal(hexGasTotal))
const fiatTransactionFee = getTransactionFee({
value: hexGasTotal,
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
})
const ethTransactionFee = getTransactionFee({
value: hexGasTotal,
toCurrency: 'ETH',
numberOfDecimals: 6,
conversionRate,
})
dispatch(updateTransactionFees({ fiatTransactionFee, ethTransactionFee }))
const fiatTransactionTotal = addFiat(fiatTransactionFee, fiatTransactionAmount)
const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount)
dispatch(updateTransactionTotals({ fiatTransactionTotal, ethTransactionTotal }))
}
}
export function setTransactionToConfirm (transactionId) {
return async (dispatch, getState) => {
const state = getState()
const unconfirmedTransactionsHash = unconfirmedTransactionsHashSelector(state)
const transaction = unconfirmedTransactionsHash[transactionId]
if (!transaction) {
console.error(`Transaction with id ${transactionId} not found`)
return
}
const { lastGasPrice } = transaction
const txData = lastGasPrice ? increaseFromLastGasPrice(transaction) : transaction
dispatch(updateTxDataAndCalculate(txData))
const { txParams } = transaction
if (txParams.data) {
const { tokens: existingTokens } = state
const { data, to: tokenAddress } = txParams
const tokenData = getTokenData(data)
dispatch(updateTokenData(tokenData))
const tokenSymbolData = await getSymbolAndDecimals(tokenAddress, existingTokens) || {}
const { symbol: tokenSymbol = '', decimals: tokenDecimals = '' } = tokenSymbolData
dispatch(updateTokenProps({ tokenSymbol, tokenDecimals }))
}
if (txParams.nonce) {
const nonce = conversionUtil(txParams.nonce, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
dispatch(updateNonce(nonce))
}
}
}
export function clearConfirmTransaction () {
return {
type: CLEAR_CONFIRM_TRANSACTION,
}
}

Some files were not shown because too many files have changed in this diff Show More