From 57a29668f3caf3716fb99b904a53ec71cbce6ecb Mon Sep 17 00:00:00 2001 From: Terry Smith <52763493+tshfx@users.noreply.github.com> Date: Mon, 4 Nov 2019 08:40:46 -0400 Subject: [PATCH] New signature request v3 UI (#6891) * Refactoring signature-request out to a new component. Wip * Styling polish and a better message display. * Update signature request header to no longer use account dropdown mini * Clean up code and styles * Code cleanup for signature request redesign branch * Fix signature request design for full screen * Replace makenode with object.entries in signature-request-message.component.js * Remove unused accounts prop from signature-request.component.js * Use beforeunload instead of window.onbeforeunload in signature-request --- app/_locales/en/messages.json | 3 + ui/app/components/app/index.scss | 2 +- ...quest.js => signature-request-original.js} | 22 ++--- .../components/app/signature-request/index.js | 1 + .../app/signature-request/index.scss | 96 +++++++++++++++++++ .../signature-request-footer/index.js | 1 + .../signature-request-footer/index.scss | 18 ++++ .../signature-request-footer.component.js | 24 +++++ .../signature-request-header/index.js | 1 + .../signature-request-header/index.scss | 25 +++++ .../signature-request-header.component.js | 29 ++++++ .../signature-request-message/index.js | 1 + .../signature-request-message/index.scss | 67 +++++++++++++ .../signature-request-message.component.js | 50 ++++++++++ .../signature-request.component.js | 81 ++++++++++++++++ .../signature-request.constants.js | 3 + .../signature-request.container.js | 72 ++++++++++++++ .../tests/signature-request.test.js | 25 +++++ ui/app/pages/confirm-transaction/conf-tx.js | 50 ++++++---- .../confirm-transaction.component.js | 2 +- .../confirm-transaction.container.js | 2 + 21 files changed, 542 insertions(+), 33 deletions(-) rename ui/app/components/app/{signature-request.js => signature-request-original.js} (94%) create mode 100644 ui/app/components/app/signature-request/index.js create mode 100644 ui/app/components/app/signature-request/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-footer/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-footer/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js create mode 100644 ui/app/components/app/signature-request/signature-request-header/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-header/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js create mode 100644 ui/app/components/app/signature-request/signature-request-message/index.js create mode 100644 ui/app/components/app/signature-request/signature-request-message/index.scss create mode 100644 ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js create mode 100644 ui/app/components/app/signature-request/signature-request.component.js create mode 100644 ui/app/components/app/signature-request/signature-request.constants.js create mode 100644 ui/app/components/app/signature-request/signature-request.container.js create mode 100644 ui/app/components/app/signature-request/tests/signature-request.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 3733830cc..d4c8caffa 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1188,6 +1188,9 @@ "signatureRequest": { "message": "Signature Request" }, + "signatureRequest1": { + "message": "Message" + }, "signed": { "message": "Signed" }, diff --git a/ui/app/components/app/index.scss b/ui/app/components/app/index.scss index 1ccb6a94a..d1ffa4949 100644 --- a/ui/app/components/app/index.scss +++ b/ui/app/components/app/index.scss @@ -84,4 +84,4 @@ @import 'home-notification/index'; -@import 'multiple-notifications/index'; +@import 'signature-request/index'; diff --git a/ui/app/components/app/signature-request.js b/ui/app/components/app/signature-request-original.js similarity index 94% rename from ui/app/components/app/signature-request.js rename to ui/app/components/app/signature-request-original.js index e7370c124..0a9a43593 100644 --- a/ui/app/components/app/signature-request.js +++ b/ui/app/components/app/signature-request-original.js @@ -243,7 +243,7 @@ SignatureRequest.prototype.renderBody = function () { let notice = this.context.t('youSign') + ':' const { txData } = this.props - const { type, msgParams: { data, version } } = txData + const { type, msgParams: { data } } = txData if (type === 'personal_sign') { rows = [{ name: this.context.t('message'), value: this.msgHexToText(data) }] @@ -275,17 +275,15 @@ SignatureRequest.prototype.renderBody = function () { }, [notice]), h('div.request-signature__rows', - type === 'eth_signTypedData' && (version === 'V3' || version === 'V4') ? - this.renderTypedData(data) : - rows.map(({ name, value }) => { - if (typeof value === 'boolean') { - value = value.toString() - } - return h('div.request-signature__row', [ - h('div.request-signature__row-title', [`${name}:`]), - h('div.request-signature__row-value', value), - ]) - }), + rows.map(({ name, value }, index) => { + if (typeof value === 'boolean') { + value = value.toString() + } + return h('div.request-signature__row', { key: `request-signature-row-${index}` }, [ + h('div.request-signature__row-title', [`${name}:`]), + h('div.request-signature__row-value', value), + ]) + }) ), ]) } diff --git a/ui/app/components/app/signature-request/index.js b/ui/app/components/app/signature-request/index.js new file mode 100644 index 000000000..b1c8a1960 --- /dev/null +++ b/ui/app/components/app/signature-request/index.js @@ -0,0 +1 @@ +export { default } from './signature-request.container' diff --git a/ui/app/components/app/signature-request/index.scss b/ui/app/components/app/signature-request/index.scss new file mode 100644 index 000000000..69115681f --- /dev/null +++ b/ui/app/components/app/signature-request/index.scss @@ -0,0 +1,96 @@ +@import 'signature-request-footer/index'; +@import 'signature-request-header/index'; +@import 'signature-request-message/index'; + +.signature-request { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + + @media screen and (min-width: 576px) { + flex: initial; + } +} + +.signature-request-header { + flex: 1; + + .network-display__container { + padding: 0; + justify-content: flex-end; + } + + .network-display__name { + font-size: 12px; + white-space: nowrap; + font-weight: 500; + } +} + +.signature-request-content { + flex: 1 40%; + margin-top: 1rem; + display: flex; + align-items: center; + flex-direction: column; + margin-bottom: 25px; + min-height: min-content; + + &__title { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-size: 18px; + } + + &__identicon-container { + padding: 1rem; + flex: 1; + position: relative; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + } + + &__identicon-border { + height: 75px; + width: 75px; + border-radius: 50%; + border: 1px solid white; + position: absolute; + box-shadow: 0 2px 2px 0.5px rgba(0, 0, 0, 0.19); + } + + &__identicon-initial { + position: absolute; + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-size: 60px; + color: white; + z-index: 1; + text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.422); + } + + &__info { + font-size: 12px; + } + + &__info--bolded { + font-size: 16px; + font-weight: 500; + } + + p { + color: #999999; + font-size: 0.8rem; + } + + .identicon {} +} + +.signature-request-footer { + flex: 1 1 auto; +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-footer/index.js b/ui/app/components/app/signature-request/signature-request-footer/index.js new file mode 100644 index 000000000..11d0b3944 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-footer.component' diff --git a/ui/app/components/app/signature-request/signature-request-footer/index.scss b/ui/app/components/app/signature-request/signature-request-footer/index.scss new file mode 100644 index 000000000..d8c6b36d6 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/index.scss @@ -0,0 +1,18 @@ +.signature-request-footer { + display: flex; + border-top: 1px solid #d2d8dd; + + button { + text-transform: uppercase; + flex: 1; + margin: 1rem 0.5rem; + border-radius: 3px; + } + + button:first-child() { + margin-left: 1rem; + } + button:last-child() { + margin-right: 1rem; + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js b/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js new file mode 100644 index 000000000..591b9a03a --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-footer/signature-request-footer.component.js @@ -0,0 +1,24 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Button from '../../../ui/button' + +export default class SignatureRequestFooter extends PureComponent { + static propTypes = { + cancelAction: PropTypes.func.isRequired, + signAction: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + render () { + const { cancelAction, signAction } = this.props + return ( +
+ + +
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request-header/index.js b/ui/app/components/app/signature-request/signature-request-header/index.js new file mode 100644 index 000000000..fa596383a --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-header.component' diff --git a/ui/app/components/app/signature-request/signature-request-header/index.scss b/ui/app/components/app/signature-request/signature-request-header/index.scss new file mode 100644 index 000000000..7a33f85f2 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/index.scss @@ -0,0 +1,25 @@ +.signature-request-header { + display: flex; + padding: 1rem; + border-bottom: 1px solid $geyser; + justify-content: space-between; + font-size: .75rem; + + &--account, &--network { + flex: 1; + } + + &--account { + display: flex; + align-items: center; + + .account-list-item__account-name { + font-size: 12px; + font-weight: 500; + } + + .account-list-item__top-row { + margin: 0px; + } + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js b/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js new file mode 100644 index 000000000..3ac0c9afb --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-header/signature-request-header.component.js @@ -0,0 +1,29 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import AccountListItem from '../../../../pages/send/account-list-item/account-list-item.component' +import NetworkDisplay from '../../network-display' + +export default class SignatureRequestHeader extends PureComponent { + static propTypes = { + selectedAccount: PropTypes.object.isRequired, + } + + render () { + const { selectedAccount } = this.props + + return ( +
+
+ {selectedAccount && } + {name} +
+
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request-message/index.js b/ui/app/components/app/signature-request/signature-request-message/index.js new file mode 100644 index 000000000..e62265a5f --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/index.js @@ -0,0 +1 @@ +export { default } from './signature-request-message.component' diff --git a/ui/app/components/app/signature-request/signature-request-message/index.scss b/ui/app/components/app/signature-request/signature-request-message/index.scss new file mode 100644 index 000000000..aec597f89 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/index.scss @@ -0,0 +1,67 @@ +.signature-request-message { + flex: 1 60%; + display: flex; + flex-direction: column; + + &__title { + font-weight: 500; + font-size: 14px; + color: #636778; + margin-left: 12px; + } + + h2 { + flex: 1 1 0; + text-align: left; + font-size: 0.8rem; + border-bottom: 1px solid #d2d8dd; + padding: 0.5rem; + margin: 0; + color: #ccc; + } + + &--root { + flex: 1 100%; + background-color: #f8f9fb; + padding-bottom: 0.5rem; + overflow: auto; + padding-left: 12px; + padding-right: 12px; + width: 360px; + font-family: monospace; + + @media screen and (min-width: 576px) { + width: auto; + } + } + + &__type-title { + font-family: monospace; + font-style: normal; + font-weight: normal; + font-size: 14px; + margin-left: 12px; + margin-top: 6px; + margin-bottom: 10px; + } + + &--node, &--node-leaf { + padding-left: 0.8rem; + + &-label { + color: #5B5D67; + } + + &-value { + color: black; + margin-left: 0.5rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + } + + &--node-leaf { + display: flex; + } +} \ No newline at end of file diff --git a/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js new file mode 100644 index 000000000..16b6c3bea --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request-message/signature-request-message.component.js @@ -0,0 +1,50 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +export default class SignatureRequestMessage extends PureComponent { + static propTypes = { + data: PropTypes.object.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + renderNode (data) { + return ( +
+ {Object.entries(data).map(([ label, value ], i) => ( +
+ {label}: + { + typeof value === 'object' && value !== null ? + this.renderNode(value) + : {value} + } +
+ ))} +
+ ) + } + + + render () { + const { data } = this.props + + return ( +
+
{this.context.t('signatureRequest1')}
+
+
{this.context.t('signatureRequest1')}
+ {this.renderNode(data)} +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request.component.js b/ui/app/components/app/signature-request/signature-request.component.js new file mode 100644 index 000000000..7029b1e00 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.component.js @@ -0,0 +1,81 @@ +import React, { PureComponent } from 'react' +import PropTypes from 'prop-types' +import Header from './signature-request-header' +import Footer from './signature-request-footer' +import Message from './signature-request-message' +import { ENVIRONMENT_TYPE_NOTIFICATION } from './signature-request.constants' +import { getEnvironmentType } from '../../../../../app/scripts/lib/util' +import Identicon from '../../ui/identicon' + +export default class SignatureRequest extends PureComponent { + static propTypes = { + txData: PropTypes.object.isRequired, + selectedAccount: PropTypes.shape({ + address: PropTypes.string, + balance: PropTypes.string, + name: PropTypes.string, + }).isRequired, + + clearConfirmTransaction: PropTypes.func.isRequired, + cancel: PropTypes.func.isRequired, + sign: PropTypes.func.isRequired, + } + + static contextTypes = { + t: PropTypes.func, + } + + componentDidMount () { + const { clearConfirmTransaction, cancel } = this.props + const { metricsEvent } = this.context + if (getEnvironmentType(window.location.href) === ENVIRONMENT_TYPE_NOTIFICATION) { + window.addEventListener('beforeunload', (event) => { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Sign Request', + name: 'Cancel Sig Request Via Notification Close', + }, + }) + clearConfirmTransaction() + cancel(event) + }) + } + } + + formatWallet (wallet) { + return `${wallet.slice(0, 8)}...${wallet.slice(wallet.length - 8, wallet.length)}` + } + + render () { + const { + selectedAccount, + txData: { msgParams: { data, origin, from: senderWallet }}, + cancel, + sign, + } = this.props + const { message } = JSON.parse(data) + + return ( +
+
+
+
{this.context.t('sigRequest')}
+
+
{ message.from.name && message.from.name[0] }
+
+ +
+
{message.from.name}
+
{origin}
+
{this.formatWallet(senderWallet)}
+
+ +
+
+ ) + } +} diff --git a/ui/app/components/app/signature-request/signature-request.constants.js b/ui/app/components/app/signature-request/signature-request.constants.js new file mode 100644 index 000000000..9cf241928 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.constants.js @@ -0,0 +1,3 @@ +import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums' + +export { ENVIRONMENT_TYPE_NOTIFICATION } diff --git a/ui/app/components/app/signature-request/signature-request.container.js b/ui/app/components/app/signature-request/signature-request.container.js new file mode 100644 index 000000000..0b09c1a64 --- /dev/null +++ b/ui/app/components/app/signature-request/signature-request.container.js @@ -0,0 +1,72 @@ +import { connect } from 'react-redux' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import SignatureRequest from './signature-request.component' +import { goHome } from '../../../store/actions' +import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck' +import { + getSelectedAccount, + getCurrentAccountWithSendEtherInfo, + getSelectedAddress, + accountsWithSendEtherInfoSelector, + conversionRateSelector, +} from '../../../selectors/selectors.js' + +function mapStateToProps (state) { + return { + balance: getSelectedAccount(state).balance, + selectedAccount: getCurrentAccountWithSendEtherInfo(state), + selectedAddress: getSelectedAddress(state), + accounts: accountsWithSendEtherInfoSelector(state), + conversionRate: conversionRateSelector(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + goHome: () => dispatch(goHome()), + clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + } +} + +function mergeProps (stateProps, dispatchProps, ownProps) { + const { + signPersonalMessage, + signTypedMessage, + cancelPersonalMessage, + cancelTypedMessage, + signMessage, + cancelMessage, + txData, + } = ownProps + + const { type } = txData + + let cancel + let sign + + if (type === 'personal_sign') { + cancel = cancelPersonalMessage + sign = signPersonalMessage + } else if (type === 'eth_signTypedData') { + cancel = cancelTypedMessage + sign = signTypedMessage + } else if (type === 'eth_sign') { + cancel = cancelMessage + sign = signMessage + } + + return { + ...stateProps, + ...dispatchProps, + ...ownProps, + txData, + cancel, + sign, + } +} + +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps, mergeProps) +)(SignatureRequest) diff --git a/ui/app/components/app/signature-request/tests/signature-request.test.js b/ui/app/components/app/signature-request/tests/signature-request.test.js new file mode 100644 index 000000000..68b114dd8 --- /dev/null +++ b/ui/app/components/app/signature-request/tests/signature-request.test.js @@ -0,0 +1,25 @@ +import React from 'react' +import assert from 'assert' +import shallow from '../../../../../lib/shallow-with-context' +import SignatureRequest from '../signature-request.component' + + +describe('Signature Request Component', function () { + let wrapper + + beforeEach(() => { + wrapper = shallow() + }) + + describe('render', () => { + it('should render a div with one child', () => { + assert(wrapper.is('div')) + assert.equal(wrapper.length, 1) + assert(wrapper.hasClass('signature-request')) + }) + }) +}) diff --git a/ui/app/pages/confirm-transaction/conf-tx.js b/ui/app/pages/confirm-transaction/conf-tx.js index 4f3868bc8..ce1edde5c 100644 --- a/ui/app/pages/confirm-transaction/conf-tx.js +++ b/ui/app/pages/confirm-transaction/conf-tx.js @@ -9,7 +9,8 @@ const txHelper = require('../../../lib/tx-helper') const log = require('loglevel') const R = require('ramda') -const SignatureRequest = require('../../components/app/signature-request') +const SignatureRequest = require('../../components/app/signature-request').default +const SignatureRequestOriginal = require('../../components/app/signature-request-original') const Loading = require('../../components/ui/loading-screen') const { DEFAULT_ROUTE } = require('../../helpers/constants/routes') @@ -137,34 +138,45 @@ ConfirmTxScreen.prototype.getTxData = function () { : unconfTxList[index] } +ConfirmTxScreen.prototype.signatureSelect = function (type, version) { + // Temporarily direct only v3 and v4 requests to new code. + if (type === 'eth_signTypedData' && (version === 'V3' || version === 'V4')) { + return SignatureRequest + } + + return SignatureRequestOriginal +} + ConfirmTxScreen.prototype.render = function () { const props = this.props const { currentCurrency, blockGasLimit, + conversionRate, } = props var txData = this.getTxData() || {} - const { msgParams } = txData + const { msgParams, type, msgParams: { version } } = txData log.debug('msgParams detected, rendering pending msg') - return msgParams - ? h(SignatureRequest, { - // Properties - txData: txData, - key: txData.id, - identities: props.identities, - currentCurrency, - blockGasLimit, - // Actions - signMessage: this.signMessage.bind(this, txData), - signPersonalMessage: this.signPersonalMessage.bind(this, txData), - signTypedMessage: this.signTypedMessage.bind(this, txData), - cancelMessage: this.cancelMessage.bind(this, txData), - cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), - cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), - }) - : h(Loading) + return msgParams ? h(this.signatureSelect(type, version), { + // Properties + txData: txData, + key: txData.id, + selectedAddress: props.selectedAddress, + accounts: props.accounts, + identities: props.identities, + conversionRate, + currentCurrency, + blockGasLimit, + // Actions + signMessage: this.signMessage.bind(this, txData), + signPersonalMessage: this.signPersonalMessage.bind(this, txData), + signTypedMessage: this.signTypedMessage.bind(this, txData), + cancelMessage: this.cancelMessage.bind(this, txData), + cancelPersonalMessage: this.cancelPersonalMessage.bind(this, txData), + cancelTypedMessage: this.cancelTypedMessage.bind(this, txData), + }) : h(Loading) } ConfirmTxScreen.prototype.signMessage = function (msgData, event) { diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.component.js b/ui/app/pages/confirm-transaction/confirm-transaction.component.js index 4b37cf2b1..9cb69e0da 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.component.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.component.js @@ -45,6 +45,7 @@ export default class ConfirmTransaction extends Component { isTokenMethodAction: PropTypes.bool, fullScreenVsPopupTestGroup: PropTypes.string, trackABTest: PropTypes.bool, + conversionRate: PropTypes.number, } componentDidMount () { @@ -118,7 +119,6 @@ export default class ConfirmTransaction extends Component { // 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/ - return transactionId && (!paramsTransactionId || paramsTransactionId === transactionId) ? ( diff --git a/ui/app/pages/confirm-transaction/confirm-transaction.container.js b/ui/app/pages/confirm-transaction/confirm-transaction.container.js index 9625db8ec..7c3986441 100644 --- a/ui/app/pages/confirm-transaction/confirm-transaction.container.js +++ b/ui/app/pages/confirm-transaction/confirm-transaction.container.js @@ -25,6 +25,7 @@ const mapStateToProps = (state, ownProps) => { send, unapprovedTxs, abTests: { fullScreenVsPopup }, + conversionRate, }, confirmTransaction, } = state @@ -53,6 +54,7 @@ const mapStateToProps = (state, ownProps) => { isTokenMethodAction: isTokenMethodAction(transactionCategory), trackABTest, fullScreenVsPopupTestGroup: fullScreenVsPopup, + conversionRate, } }