diff options
author | brunobar79 <brunobar79@gmail.com> | 2018-07-13 00:54:08 +0800 |
---|---|---|
committer | brunobar79 <brunobar79@gmail.com> | 2018-07-13 00:54:08 +0800 |
commit | 07d8bfaec51a798e090bd2521debeddaf53bf2f9 (patch) | |
tree | 735338e1dbaa38601376024ed6e4fa4d89c66a2c /ui/app/components/pages | |
parent | 2a0a7853249284cb27831890f3b62847ea27eb83 (diff) | |
parent | 0d4dbbec2abfa8c8015063d6e4a5ff0d34abe7b9 (diff) | |
download | tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.tar tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.tar.gz tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.tar.bz2 tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.tar.lz tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.tar.xz tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.tar.zst tangerine-wallet-browser-07d8bfaec51a798e090bd2521debeddaf53bf2f9.zip |
Merge branch 'develop' of github.com:MetaMask/metamask-extension into initial-trezor-support
Diffstat (limited to 'ui/app/components/pages')
27 files changed, 1170 insertions, 130 deletions
diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.component.js b/ui/app/components/pages/confirm-approve/confirm-approve.component.js new file mode 100644 index 000000000..d775b0362 --- /dev/null +++ b/ui/app/components/pages/confirm-approve/confirm-approve.component.js @@ -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 + /> + ) + } +} diff --git a/ui/app/components/pages/confirm-approve/confirm-approve.container.js b/ui/app/components/pages/confirm-approve/confirm-approve.container.js new file mode 100644 index 000000000..040e499ae --- /dev/null +++ b/ui/app/components/pages/confirm-approve/confirm-approve.container.js @@ -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) diff --git a/ui/app/components/pages/confirm-approve/index.js b/ui/app/components/pages/confirm-approve/index.js new file mode 100644 index 000000000..791297be7 --- /dev/null +++ b/ui/app/components/pages/confirm-approve/index.js @@ -0,0 +1 @@ +export { default } from './confirm-approve.container' diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js new file mode 100644 index 000000000..9bc0daab9 --- /dev/null +++ b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.component.js @@ -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()} + /> + ) + } +} diff --git a/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js new file mode 100644 index 000000000..336ee83ea --- /dev/null +++ b/ui/app/components/pages/confirm-deploy-contract/confirm-deploy-contract.container.js @@ -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) diff --git a/ui/app/components/pages/confirm-deploy-contract/index.js b/ui/app/components/pages/confirm-deploy-contract/index.js new file mode 100644 index 000000000..c4fb01b52 --- /dev/null +++ b/ui/app/components/pages/confirm-deploy-contract/index.js @@ -0,0 +1 @@ +export { default } from './confirm-deploy-contract.container' diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js new file mode 100644 index 000000000..442a478b8 --- /dev/null +++ b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.component.js @@ -0,0 +1,39 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ConfirmTransactionBase from '../confirm-transaction-base' +import { SEND_ROUTE } from '../../../routes' + +export default class ConfirmSendEther extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + editTransaction: PropTypes.func, + history: PropTypes.object, + txParams: PropTypes.object, + } + + handleEdit ({ txData }) { + const { editTransaction, history } = this.props + editTransaction(txData) + history.push(SEND_ROUTE) + } + + shouldHideData () { + const { txParams = {} } = this.props + return !txParams.data + } + + render () { + const hideData = this.shouldHideData() + + return ( + <ConfirmTransactionBase + action={this.context.t('confirm')} + hideData={hideData} + onEdit={confirmTransactionData => this.handleEdit(confirmTransactionData)} + /> + ) + } +} diff --git a/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js new file mode 100644 index 000000000..e48ef54a8 --- /dev/null +++ b/ui/app/components/pages/confirm-send-ether/confirm-send-ether.container.js @@ -0,0 +1,45 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import { updateSend } from '../../../actions' +import { clearConfirmTransaction } from '../../../ducks/confirm-transaction.duck' +import ConfirmSendEther from './confirm-send-ether.component' + +const mapStateToProps = state => { + const { confirmTransaction: { txData: { txParams } = {} } } = state + + return { + txParams, + } +} + +const mapDispatchToProps = dispatch => { + return { + editTransaction: txData => { + const { id, txParams } = txData + const { + gas: gasLimit, + gasPrice, + to, + value: amount, + } = txParams + + dispatch(updateSend({ + gasLimit, + gasPrice, + gasTotal: null, + to, + amount, + errors: { to: null, amount: null }, + editingTransactionId: id && id.toString(), + })) + + dispatch(clearConfirmTransaction()) + }, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(ConfirmSendEther) diff --git a/ui/app/components/pages/confirm-send-ether/index.js b/ui/app/components/pages/confirm-send-ether/index.js new file mode 100644 index 000000000..2d5767c39 --- /dev/null +++ b/ui/app/components/pages/confirm-send-ether/index.js @@ -0,0 +1 @@ +export { default } from './confirm-send-ether.container' diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js new file mode 100644 index 000000000..46ad9ccab --- /dev/null +++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.component.js @@ -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 + /> + ) + } +} diff --git a/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js new file mode 100644 index 000000000..2d7efeed6 --- /dev/null +++ b/ui/app/components/pages/confirm-send-token/confirm-send-token.container.js @@ -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) diff --git a/ui/app/components/pages/confirm-send-token/index.js b/ui/app/components/pages/confirm-send-token/index.js new file mode 100644 index 000000000..409b6ef3d --- /dev/null +++ b/ui/app/components/pages/confirm-send-token/index.js @@ -0,0 +1 @@ +export { default } from './confirm-send-token.container' diff --git a/ui/app/components/pages/confirm-send-token/index.scss b/ui/app/components/pages/confirm-send-token/index.scss new file mode 100644 index 000000000..0476749f6 --- /dev/null +++ b/ui/app/components/pages/confirm-send-token/index.scss @@ -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; + } +} diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js new file mode 100644 index 000000000..842b34d2e --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -0,0 +1,320 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import ConfirmPageContainer, { ConfirmDetailRow } from '../../confirm-page-container' +import { formatCurrency } from '../../../helpers/confirm-transaction/util' +import { isBalanceSufficient } from '../../send_/send.utils' +import { DEFAULT_ROUTE } from '../../../routes' +import { + INSUFFICIENT_FUNDS_ERROR_KEY, + TRANSACTION_ERROR_KEY, +} from '../../../constants/error-keys' + +export default class ConfirmTransactionBase extends Component { + static contextTypes = { + t: PropTypes.func, + } + + static propTypes = { + // react-router props + match: PropTypes.object, + history: PropTypes.object, + // Redux props + balance: PropTypes.string, + cancelTransaction: PropTypes.func, + clearConfirmTransaction: PropTypes.func, + clearSend: PropTypes.func, + conversionRate: PropTypes.number, + currentCurrency: PropTypes.string, + editTransaction: PropTypes.func, + ethTransactionAmount: PropTypes.string, + ethTransactionFee: PropTypes.string, + ethTransactionTotal: PropTypes.string, + fiatTransactionAmount: PropTypes.string, + fiatTransactionFee: PropTypes.string, + fiatTransactionTotal: PropTypes.string, + fromAddress: PropTypes.string, + fromName: PropTypes.string, + hexGasTotal: PropTypes.string, + isTxReprice: PropTypes.bool, + methodData: PropTypes.object, + nonce: PropTypes.string, + sendTransaction: PropTypes.func, + showCustomizeGasModal: PropTypes.func, + showTransactionConfirmedModal: PropTypes.func, + toAddress: PropTypes.string, + tokenData: PropTypes.object, + tokenProps: PropTypes.object, + toName: PropTypes.string, + transactionStatus: PropTypes.string, + txData: PropTypes.object, + // Component props + action: PropTypes.string, + contentComponent: PropTypes.node, + dataComponent: PropTypes.node, + detailsComponent: PropTypes.node, + errorKey: PropTypes.string, + errorMessage: PropTypes.string, + hideData: PropTypes.bool, + hideDetails: PropTypes.bool, + hideSubtitle: PropTypes.bool, + identiconAddress: PropTypes.string, + onCancel: PropTypes.func, + onEdit: PropTypes.func, + onEditGas: PropTypes.func, + onSubmit: PropTypes.func, + subtitle: PropTypes.string, + summaryComponent: PropTypes.node, + title: PropTypes.string, + valid: PropTypes.bool, + warning: PropTypes.string, + } + + componentDidUpdate () { + const { + transactionStatus, + showTransactionConfirmedModal, + history, + clearConfirmTransaction, + } = this.props + + if (transactionStatus === 'dropped') { + showTransactionConfirmedModal({ + onHide: () => { + clearConfirmTransaction() + history.push(DEFAULT_ROUTE) + }, + }) + + return + } + } + + getErrorKey () { + const { + balance, + conversionRate, + hexGasTotal, + txData: { + simulationFails, + txParams: { + value: amount, + } = {}, + } = {}, + } = this.props + + const insufficientBalance = balance && !isBalanceSufficient({ + amount, + gasTotal: hexGasTotal || '0x0', + balance, + conversionRate, + }) + + if (insufficientBalance) { + return { + valid: false, + errorKey: INSUFFICIENT_FUNDS_ERROR_KEY, + } + } + + if (simulationFails) { + return { + valid: false, + errorKey: TRANSACTION_ERROR_KEY, + } + } + + return { + valid: true, + } + } + + handleEditGas () { + const { onEditGas, showCustomizeGasModal } = this.props + + if (onEditGas) { + onEditGas() + } else { + showCustomizeGasModal() + } + } + + renderDetails () { + const { + detailsComponent, + fiatTransactionFee, + ethTransactionFee, + currentCurrency, + fiatTransactionTotal, + ethTransactionTotal, + hideDetails, + } = this.props + + if (hideDetails) { + return null + } + + return ( + detailsComponent || ( + <div className="confirm-page-container-content__details"> + <div className="confirm-page-container-content__gas-fee"> + <ConfirmDetailRow + label="Gas Fee" + fiatFee={formatCurrency(fiatTransactionFee, currentCurrency)} + ethFee={ethTransactionFee} + headerText="Edit" + headerTextClassName="confirm-detail-row__header-text--edit" + onHeaderClick={() => this.handleEditGas()} + /> + </div> + <div> + <ConfirmDetailRow + label="Total" + fiatFee={formatCurrency(fiatTransactionTotal, currentCurrency)} + ethFee={ethTransactionTotal} + headerText="Amount + Gas Fee" + headerTextClassName="confirm-detail-row__header-text--total" + fiatFeeColor="#2f9ae0" + /> + </div> + </div> + ) + ) + } + + renderData () { + const { t } = this.context + const { + txData: { + txParams: { + data, + } = {}, + } = {}, + methodData: { + name, + params, + } = {}, + hideData, + dataComponent, + } = this.props + + if (hideData) { + return null + } + + return dataComponent || ( + <div className="confirm-page-container-content__data"> + <div className="confirm-page-container-content__data-box-label"> + {`${t('functionType')}:`} + <span className="confirm-page-container-content__function-type"> + { name } + </span> + </div> + <div className="confirm-page-container-content__data-box"> + <div className="confirm-page-container-content__data-field-label"> + { `${t('parameters')}:` } + </div> + <div> + <pre>{ JSON.stringify(params, null, 2) }</pre> + </div> + </div> + <div className="confirm-page-container-content__data-box-label"> + {`${t('hexData')}:`} + </div> + <div className="confirm-page-container-content__data-box"> + { data } + </div> + </div> + ) + } + + handleEdit () { + const { txData, tokenData, tokenProps, onEdit } = this.props + onEdit({ txData, tokenData, tokenProps }) + } + + handleCancel () { + const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props + + if (onCancel) { + onCancel(txData) + } else { + cancelTransaction(txData) + .then(() => { + clearConfirmTransaction() + history.push(DEFAULT_ROUTE) + }) + } + } + + handleSubmit () { + const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props + + if (onSubmit) { + onSubmit(txData) + } else { + sendTransaction(txData) + .then(() => { + clearConfirmTransaction() + history.push(DEFAULT_ROUTE) + }) + } + } + + render () { + const { + isTxReprice, + fromName, + fromAddress, + toName, + toAddress, + methodData, + ethTransactionAmount, + fiatTransactionAmount, + valid: propsValid, + errorMessage, + errorKey: propsErrorKey, + currentCurrency, + action, + title, + subtitle, + hideSubtitle, + identiconAddress, + summaryComponent, + contentComponent, + onEdit, + nonce, + warning, + } = this.props + + const { name } = methodData + const fiatConvertedAmount = formatCurrency(fiatTransactionAmount, currentCurrency) + const { valid, errorKey } = this.getErrorKey() + + return ( + <ConfirmPageContainer + fromName={fromName} + fromAddress={fromAddress} + toName={toName} + toAddress={toAddress} + showEdit={onEdit && !isTxReprice} + action={action || name} + title={title || `${fiatConvertedAmount} ${currentCurrency.toUpperCase()}`} + subtitle={subtitle || `\u2666 ${ethTransactionAmount}`} + hideSubtitle={hideSubtitle} + summaryComponent={summaryComponent} + detailsComponent={this.renderDetails()} + dataComponent={this.renderData()} + contentComponent={contentComponent} + nonce={nonce} + identiconAddress={identiconAddress} + errorMessage={errorMessage} + errorKey={propsErrorKey || errorKey} + warning={warning} + valid={propsValid || valid} + onEdit={() => this.handleEdit()} + onCancel={() => this.handleCancel()} + onSubmit={() => this.handleSubmit()} + /> + ) + } +} diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js new file mode 100644 index 000000000..31108bbd0 --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -0,0 +1,169 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import R from 'ramda' +import ConfirmTransactionBase from './confirm-transaction-base.component' +import { + clearConfirmTransaction, + updateGasAndCalculate, +} from '../../../ducks/confirm-transaction.duck' +import { clearSend, cancelTx, updateAndApproveTx, showModal } from '../../../actions' +import { + INSUFFICIENT_FUNDS_ERROR_KEY, + GAS_LIMIT_TOO_LOW_ERROR_KEY, +} from '../../../constants/error-keys' +import { getHexGasTotal } from '../../../helpers/confirm-transaction/util' +import { isBalanceSufficient } from '../../send_/send.utils' +import { conversionGreaterThan } from '../../../conversion-util' +import { MIN_GAS_LIMIT_DEC } from '../../send_/send.constants' + +const mapStateToProps = (state, props) => { + const { toAddress: propsToAddress } = props + const { confirmTransaction, metamask } = state + const { + ethTransactionAmount, + ethTransactionFee, + ethTransactionTotal, + fiatTransactionAmount, + fiatTransactionFee, + fiatTransactionTotal, + hexGasTotal, + tokenData, + methodData, + txData, + tokenProps, + nonce, + } = confirmTransaction + const { txParams = {}, lastGasPrice, id: transactionId } = txData + const { from: fromAddress, to: txParamsToAddress } = txParams + const { + conversionRate, + identities, + currentCurrency, + accounts, + selectedAddress, + selectedAddressTxList, + } = metamask + + const { balance } = accounts[selectedAddress] + const { name: fromName } = identities[selectedAddress] + const toAddress = propsToAddress || txParamsToAddress + const toName = identities[toAddress] && identities[toAddress].name + const isTxReprice = Boolean(lastGasPrice) + + const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList) + const transactionStatus = transaction ? transaction.status : '' + + return { + balance, + fromAddress, + fromName, + toAddress, + toName, + ethTransactionAmount, + ethTransactionFee, + ethTransactionTotal, + fiatTransactionAmount, + fiatTransactionFee, + fiatTransactionTotal, + hexGasTotal, + txData, + tokenData, + methodData, + tokenProps, + isTxReprice, + currentCurrency, + conversionRate, + transactionStatus, + nonce, + } +} + +const mapDispatchToProps = dispatch => { + return { + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + clearSend: () => dispatch(clearSend()), + showTransactionConfirmedModal: ({ onHide }) => { + return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onHide })) + }, + showCustomizeGasModal: ({ txData, onSubmit, validate }) => { + return dispatch(showModal({ name: 'CONFIRM_CUSTOMIZE_GAS', txData, onSubmit, validate })) + }, + updateGasAndCalculate: ({ gasLimit, gasPrice }) => { + return dispatch(updateGasAndCalculate({ gasLimit, gasPrice })) + }, + cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), + sendTransaction: txData => dispatch(updateAndApproveTx(txData)), + } +} + +const getValidateEditGas = ({ balance, conversionRate, txData }) => { + const { txParams: { value: amount } = {} } = txData + + return ({ gasLimit, gasPrice }) => { + const gasTotal = getHexGasTotal({ gasLimit, gasPrice }) + const hasSufficientBalance = isBalanceSufficient({ + amount, + gasTotal, + balance, + conversionRate, + }) + + if (!hasSufficientBalance) { + return { + valid: false, + errorKey: INSUFFICIENT_FUNDS_ERROR_KEY, + } + } + + const gasLimitTooLow = gasLimit && conversionGreaterThan( + { + value: MIN_GAS_LIMIT_DEC, + fromNumericBase: 'dec', + conversionRate, + }, + { + value: gasLimit, + fromNumericBase: 'hex', + }, + ) + + if (gasLimitTooLow) { + return { + valid: false, + errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY, + } + } + + return { + valid: true, + } + } +} + +const mergeProps = (stateProps, dispatchProps, ownProps) => { + const { balance, conversionRate, txData } = stateProps + const { + showCustomizeGasModal: dispatchShowCustomizeGasModal, + updateGasAndCalculate: dispatchUpdateGasAndCalculate, + ...otherDispatchProps + } = dispatchProps + + const validateEditGas = getValidateEditGas({ balance, conversionRate, txData }) + + return { + ...stateProps, + ...otherDispatchProps, + ...ownProps, + showCustomizeGasModal: () => dispatchShowCustomizeGasModal({ + txData, + onSubmit: txData => dispatchUpdateGasAndCalculate(txData), + validate: validateEditGas, + }), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(ConfirmTransactionBase) diff --git a/ui/app/components/pages/confirm-transaction-base/index.js b/ui/app/components/pages/confirm-transaction-base/index.js new file mode 100644 index 000000000..9996e9aeb --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-base/index.js @@ -0,0 +1 @@ +export { default } from './confirm-transaction-base.container' diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js new file mode 100644 index 000000000..25259b98c --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.component.js @@ -0,0 +1,77 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { Redirect } from 'react-router-dom' +import Loading from '../../loading-screen' +import { + CONFIRM_TRANSACTION_ROUTE, + CONFIRM_DEPLOY_CONTRACT_PATH, + CONFIRM_SEND_ETHER_PATH, + CONFIRM_SEND_TOKEN_PATH, + CONFIRM_APPROVE_PATH, + CONFIRM_TOKEN_METHOD_PATH, + SIGNATURE_REQUEST_PATH, +} from '../../../routes' +import { isConfirmDeployContract } from './confirm-transaction-switch.util' +import { TOKEN_METHOD_TRANSFER, TOKEN_METHOD_APPROVE } from './confirm-transaction-switch.constants' + +export default class ConfirmTransactionSwitch extends Component { + static propTypes = { + txData: PropTypes.object, + methodData: PropTypes.object, + fetchingMethodData: PropTypes.bool, + } + + redirectToTransaction () { + const { + txData, + methodData: { name }, + fetchingMethodData, + } = this.props + const { id } = txData + + + if (isConfirmDeployContract(txData)) { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_DEPLOY_CONTRACT_PATH}` + return <Redirect to={{ pathname }} /> + } + + if (fetchingMethodData) { + return <Loading /> + } + + if (name) { + const methodName = name.toLowerCase() + + switch (methodName.toLowerCase()) { + case TOKEN_METHOD_TRANSFER: { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_TOKEN_PATH}` + return <Redirect to={{ pathname }} /> + } + case TOKEN_METHOD_APPROVE: { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}` + return <Redirect to={{ pathname }} /> + } + default: { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TOKEN_METHOD_PATH}` + return <Redirect to={{ pathname }} /> + } + } + } + + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SEND_ETHER_PATH}` + return <Redirect to={{ pathname }} /> + } + + render () { + const { txData } = this.props + + if (txData.txParams) { + return this.redirectToTransaction() + } else if (txData.msgParams) { + const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${txData.id}${SIGNATURE_REQUEST_PATH}` + return <Redirect to={{ pathname }} /> + } + + return <Loading /> + } +} diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js new file mode 100644 index 000000000..622d2a37a --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.constants.js @@ -0,0 +1,2 @@ +export const TOKEN_METHOD_TRANSFER = 'transfer' +export const TOKEN_METHOD_APPROVE = 'approve' diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js new file mode 100644 index 000000000..3d7fc78cc --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.container.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux' +import ConfirmTransactionSwitch from './confirm-transaction-switch.component' + +const mapStateToProps = state => { + const { + confirmTransaction: { + txData, + methodData, + fetchingMethodData, + }, + } = state + + return { + txData, + methodData, + fetchingMethodData, + } +} + +export default connect(mapStateToProps)(ConfirmTransactionSwitch) diff --git a/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js new file mode 100644 index 000000000..536aa5212 --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-switch/confirm-transaction-switch.util.js @@ -0,0 +1,4 @@ +export function isConfirmDeployContract (txData = {}) { + const { txParams = {} } = txData + return !txParams.to +} diff --git a/ui/app/components/pages/confirm-transaction-switch/index.js b/ui/app/components/pages/confirm-transaction-switch/index.js new file mode 100644 index 000000000..c288acb1a --- /dev/null +++ b/ui/app/components/pages/confirm-transaction-switch/index.js @@ -0,0 +1,2 @@ +import ConfirmTransactionSwitch from './confirm-transaction-switch.container' +module.exports = ConfirmTransactionSwitch diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js new file mode 100644 index 000000000..874a89fd2 --- /dev/null +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.component.js @@ -0,0 +1,150 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { Switch, Route } from 'react-router-dom' +import Loading from '../../loading-screen' +import ConfirmTransactionSwitch from '../confirm-transaction-switch' +import ConfirmTransactionBase from '../confirm-transaction-base' +import ConfirmSendEther from '../confirm-send-ether' +import ConfirmSendToken from '../confirm-send-token' +import ConfirmDeployContract from '../confirm-deploy-contract' +import ConfirmApprove from '../confirm-approve' +import ConfTx from '../../../conf-tx' +import { + DEFAULT_ROUTE, + CONFIRM_TRANSACTION_ROUTE, + CONFIRM_DEPLOY_CONTRACT_PATH, + CONFIRM_SEND_ETHER_PATH, + CONFIRM_SEND_TOKEN_PATH, + CONFIRM_APPROVE_PATH, + CONFIRM_TOKEN_METHOD_PATH, + SIGNATURE_REQUEST_PATH, +} from '../../../routes' + +export default class ConfirmTransaction extends Component { + static propTypes = { + history: PropTypes.object.isRequired, + totalUnapprovedCount: PropTypes.number.isRequired, + match: PropTypes.object, + send: PropTypes.object, + unconfirmedTransactions: PropTypes.array, + setTransactionToConfirm: PropTypes.func, + confirmTransaction: PropTypes.object, + clearConfirmTransaction: PropTypes.func, + } + + getParamsTransactionId () { + const { match: { params: { id } = {} } } = this.props + return id || null + } + + componentDidMount () { + const { + totalUnapprovedCount = 0, + send = {}, + history, + confirmTransaction: { txData: { id: transactionId } = {} }, + } = this.props + + if (!totalUnapprovedCount && !send.to) { + history.replace(DEFAULT_ROUTE) + return + } + + if (!transactionId) { + this.setTransactionToConfirm() + } + } + + componentDidUpdate () { + const { + setTransactionToConfirm, + confirmTransaction: { txData: { id: transactionId } = {} }, + clearConfirmTransaction, + } = this.props + const paramsTransactionId = this.getParamsTransactionId() + + if (paramsTransactionId && transactionId && paramsTransactionId !== transactionId + '') { + clearConfirmTransaction() + setTransactionToConfirm(paramsTransactionId) + return + } + + if (!transactionId) { + this.setTransactionToConfirm() + } + } + + setTransactionToConfirm () { + const { + history, + unconfirmedTransactions, + setTransactionToConfirm, + } = this.props + const paramsTransactionId = this.getParamsTransactionId() + + if (paramsTransactionId) { + // Check to make sure params ID is valid + const tx = unconfirmedTransactions.find(({ id }) => id + '' === paramsTransactionId) + + if (!tx) { + history.replace(DEFAULT_ROUTE) + } else { + setTransactionToConfirm(paramsTransactionId) + } + } else if (unconfirmedTransactions.length) { + const totalUnconfirmed = unconfirmedTransactions.length + const transaction = unconfirmedTransactions[totalUnconfirmed - 1] + const { id: transactionId, loadingDefaults } = transaction + + if (!loadingDefaults) { + setTransactionToConfirm(transactionId) + } + } + } + + render () { + const { confirmTransaction: { txData: { id } } = {} } = this.props + const paramsTransactionId = this.getParamsTransactionId() + + // Show routes when state.confirmTransaction has been set and when either the ID in the params + // isn't specified or is specified and matches the ID in state.confirmTransaction in order to + // support URLs of /confirm-transaction or /confirm-transaction/<transactionId> + return id && (!paramsTransactionId || paramsTransactionId === id + '') + ? ( + <Switch> + <Route + exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_DEPLOY_CONTRACT_PATH}`} + component={ConfirmDeployContract} + /> + <Route + exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TOKEN_METHOD_PATH}`} + component={ConfirmTransactionBase} + /> + <Route + exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_ETHER_PATH}`} + component={ConfirmSendEther} + /> + <Route + exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SEND_TOKEN_PATH}`} + component={ConfirmSendToken} + /> + <Route + exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_APPROVE_PATH}`} + component={ConfirmApprove} + /> + <Route + exact + path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${SIGNATURE_REQUEST_PATH}`} + component={ConfTx} + /> + <Route path="*" component={ConfirmTransactionSwitch} /> + </Switch> + ) + : <Loading /> + } +} diff --git a/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js new file mode 100644 index 000000000..1bc2f1efb --- /dev/null +++ b/ui/app/components/pages/confirm-transaction/confirm-transaction.container.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux' +import { compose } from 'recompose' +import { withRouter } from 'react-router-dom' +import { + setTransactionToConfirm, + clearConfirmTransaction, +} from '../../../ducks/confirm-transaction.duck' +import ConfirmTransaction from './confirm-transaction.component' +import { getTotalUnapprovedCount } from '../../../selectors' +import { unconfirmedTransactionsListSelector } from '../../../selectors/confirm-transaction' + +const mapStateToProps = state => { + const { metamask: { send }, confirmTransaction } = state + + return { + totalUnapprovedCount: getTotalUnapprovedCount(state), + send, + confirmTransaction, + unconfirmedTransactions: unconfirmedTransactionsListSelector(state), + } +} + +const mapDispatchToProps = dispatch => { + return { + setTransactionToConfirm: transactionId => dispatch(setTransactionToConfirm(transactionId)), + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), +)(ConfirmTransaction) diff --git a/ui/app/components/pages/confirm-transaction/index.js b/ui/app/components/pages/confirm-transaction/index.js new file mode 100644 index 000000000..4bf42d85c --- /dev/null +++ b/ui/app/components/pages/confirm-transaction/index.js @@ -0,0 +1,2 @@ +import ConfirmTransaction from './confirm-transaction.container' +module.exports = ConfirmTransaction diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js index c53413d3b..86bd32c8a 100644 --- a/ui/app/components/pages/home.js +++ b/ui/app/components/pages/home.js @@ -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 switch (currentView.name) { @@ -135,59 +90,10 @@ class Home extends Component { log.debug('rendering main container') 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': log.debug('rendering new keychain screen') 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': log.debug('rendering buy ether screen') return h(BuyView, {key: 'buyEthView'}) diff --git a/ui/app/components/pages/index.scss b/ui/app/components/pages/index.scss index b15c59863..8b333b6a8 100644 --- a/ui/app/components/pages/index.scss +++ b/ui/app/components/pages/index.scss @@ -3,3 +3,5 @@ @import './add-token/index'; @import './confirm-add-token/index'; + +@import './confirm-send-token/index'; diff --git a/ui/app/components/pages/unlock-page/unlock-page.component.js b/ui/app/components/pages/unlock-page/unlock-page.component.js index a1d3f9181..94915df76 100644 --- a/ui/app/components/pages/unlock-page/unlock-page.component.js +++ b/ui/app/components/pages/unlock-page/unlock-page.component.js @@ -2,19 +2,27 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import Button from '@material-ui/core/Button' import TextField from '../../text-field' - -const { ENVIRONMENT_TYPE_POPUP } = require('../../../../../app/scripts/lib/enums') -const { getEnvironmentType } = require('../../../../../app/scripts/lib/util') -const getCaretCoordinates = require('textarea-caret') -const EventEmitter = require('events').EventEmitter -const Mascot = require('../../mascot') -const { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } = require('../../../routes') - -class UnlockPage extends Component { +import { ENVIRONMENT_TYPE_POPUP } from '../../../../../app/scripts/lib/enums' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import getCaretCoordinates from 'textarea-caret' +import { EventEmitter } from 'events' +import Mascot from '../../mascot' +import { DEFAULT_ROUTE, RESTORE_VAULT_ROUTE } from '../../../routes' + +export default class UnlockPage extends Component { static contextTypes = { t: PropTypes.func, } + static propTypes = { + forgotPassword: PropTypes.func, + tryUnlockMetamask: PropTypes.func, + markPasswordForgotten: PropTypes.func, + history: PropTypes.object, + isUnlocked: PropTypes.bool, + useOldInterface: PropTypes.func, + } + constructor (props) { super(props) @@ -23,6 +31,7 @@ class UnlockPage extends Component { error: null, } + this.submitting = false this.animationEventEmitter = new EventEmitter() } @@ -41,20 +50,21 @@ class UnlockPage extends Component { const { password } = this.state const { tryUnlockMetamask, history } = this.props - if (password === '') { + if (password === '' || this.submitting) { return } this.setState({ error: null }) + this.submitting = true try { await tryUnlockMetamask(password) + this.submitting = false + history.push(DEFAULT_ROUTE) } catch ({ message }) { this.setState({ error: message }) - return + this.submitting = false } - - history.push(DEFAULT_ROUTE) } handleInputChange ({ target }) { @@ -98,7 +108,9 @@ class UnlockPage extends Component { } render () { - const { error } = this.state + const { password, error } = this.state + const { t } = this.context + const { markPasswordForgotten, history } = this.props return ( <div className="unlock-page__container"> @@ -111,18 +123,18 @@ class UnlockPage extends Component { /> </div> <h1 className="unlock-page__title"> - { this.context.t('welcomeBack') } + { t('welcomeBack') } </h1> - <div>{ this.context.t('unlockMessage') }</div> + <div>{ t('unlockMessage') }</div> <form className="unlock-page__form" onSubmit={event => this.handleSubmit(event)} > <TextField id="password" - label={this.context.t('password')} + label={t('password')} type="password" - value={this.state.password} + value={password} onChange={event => this.handleInputChange(event)} error={error} autoFocus @@ -136,28 +148,28 @@ class UnlockPage extends Component { <div className="unlock-page__link" onClick={() => { - this.props.markPasswordForgotten() - this.props.history.push(RESTORE_VAULT_ROUTE) + markPasswordForgotten() + history.push(RESTORE_VAULT_ROUTE) if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { global.platform.openExtensionInBrowser() } }} > - { this.context.t('restoreFromSeed') } + { t('restoreFromSeed') } </div> <div className="unlock-page__link unlock-page__link--import" onClick={() => { - this.props.markPasswordForgotten() - this.props.history.push(RESTORE_VAULT_ROUTE) + markPasswordForgotten() + history.push(RESTORE_VAULT_ROUTE) if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_POPUP) { global.platform.openExtensionInBrowser() } }} > - { this.context.t('importUsingSeed') } + { t('importUsingSeed') } </div> </div> </div> @@ -165,15 +177,3 @@ class UnlockPage extends Component { ) } } - -UnlockPage.propTypes = { - forgotPassword: PropTypes.func, - tryUnlockMetamask: PropTypes.func, - markPasswordForgotten: PropTypes.func, - history: PropTypes.object, - isUnlocked: PropTypes.bool, - t: PropTypes.func, - useOldInterface: PropTypes.func, -} - -export default UnlockPage |