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

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
This commit is contained in:
Terry Smith 2019-11-04 08:40:46 -04:00 committed by Dan J Miller
parent eed4a9ed65
commit 57a29668f3
21 changed files with 542 additions and 33 deletions

View File

@ -1188,6 +1188,9 @@
"signatureRequest": {
"message": "Signature Request"
},
"signatureRequest1": {
"message": "Message"
},
"signed": {
"message": "Signed"
},

View File

@ -84,4 +84,4 @@
@import 'home-notification/index';
@import 'multiple-notifications/index';
@import 'signature-request/index';

View File

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

View File

@ -0,0 +1 @@
export { default } from './signature-request.container'

View File

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

View File

@ -0,0 +1 @@
export { default } from './signature-request-footer.component'

View File

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

View File

@ -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 (
<div className="signature-request-footer">
<Button onClick={cancelAction} type="default" large>{this.context.t('cancel')}</Button>
<Button onClick={signAction} type="primary" large>{this.context.t('sign')}</Button>
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './signature-request-header.component'

View File

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

View File

@ -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 (
<div className="signature-request-header">
<div className="signature-request-header--account">
{selectedAccount && <AccountListItem
displayBalance={false}
account={selectedAccount}
/>}
{name}
</div>
<div className="signature-request-header--network">
<NetworkDisplay colored={false} />
</div>
</div>
)
}
}

View File

@ -0,0 +1 @@
export { default } from './signature-request-message.component'

View File

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

View File

@ -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 (
<div className="signature-request-message--node">
{Object.entries(data).map(([ label, value ], i) => (
<div
className={classnames('signature-request-message--node', {
'signature-request-message--node-leaf': typeof value !== 'object' || value === null,
})}
key={i}
>
<span className="signature-request-message--node-label">{label}: </span>
{
typeof value === 'object' && value !== null ?
this.renderNode(value)
: <span className="signature-request-message--node-value">{value}</span>
}
</div>
))}
</div>
)
}
render () {
const { data } = this.props
return (
<div className="signature-request-message">
<div className="signature-request-message__title">{this.context.t('signatureRequest1')}</div>
<div className="signature-request-message--root">
<div className="signature-request-message__type-title">{this.context.t('signatureRequest1')}</div>
{this.renderNode(data)}
</div>
</div>
)
}
}

View File

@ -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 (
<div className="signature-request page-container">
<Header selectedAccount={selectedAccount} />
<div className="signature-request-content">
<div className="signature-request-content__title">{this.context.t('sigRequest')}</div>
<div className="signature-request-content__identicon-container">
<div className="signature-request-content__identicon-initial" >{ message.from.name && message.from.name[0] }</div>
<div className="signature-request-content__identicon-border" />
<Identicon
address={message.from.wallet}
diameter={70}
/>
</div>
<div className="signature-request-content__info--bolded">{message.from.name}</div>
<div className="signature-request-content__info">{origin}</div>
<div className="signature-request-content__info">{this.formatWallet(senderWallet)}</div>
</div>
<Message data={message} />
<Footer cancelAction={cancel} signAction={sign} />
</div>
)
}
}

View File

@ -0,0 +1,3 @@
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../../../app/scripts/lib/enums'
export { ENVIRONMENT_TYPE_NOTIFICATION }

View File

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

View File

@ -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(<SignatureRequest txData={{
msgParams: {
data: '{"message": {"from": {"name": "hello"}}}',
from: '0x123456789abcdef',
} }} />)
})
describe('render', () => {
it('should render a div with one child', () => {
assert(wrapper.is('div'))
assert.equal(wrapper.length, 1)
assert(wrapper.hasClass('signature-request'))
})
})
})

View File

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

View File

@ -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/<transactionId>
return transactionId && (!paramsTransactionId || paramsTransactionId === transactionId)
? (
<Switch>

View File

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