Update Connect Request screen design (#5644)

* Parameterize NetworkDisplay background colour

* Update design for login request screen

* Pass siteTitle, siteImage through for calls to ethereum.enable()

* Bring the site images closer together
feature/default_network_editable
Whymarrh Whitby 6 years ago committed by Dan Finlay
parent 31cb111d2e
commit 26ada8a828
  1. 12
      app/_locales/en/messages.json
  2. 7
      app/images/mm-secure.svg
  3. 20
      app/images/provider-approval-check.svg
  4. 30
      app/scripts/contentscript.js
  5. 12
      app/scripts/controllers/provider-approval.js
  6. 2
      test/e2e/beta/metamask-beta-ui.spec.js
  7. 2
      ui/app/components/index.scss
  8. 5
      ui/app/components/network-display/index.scss
  9. 17
      ui/app/components/network-display/network-display.component.js
  10. 4
      ui/app/components/pages/home/home.component.js
  11. 31
      ui/app/components/pages/provider-approval/provider-approval.component.js
  12. 3
      ui/app/components/provider-page-container/index.js
  13. 120
      ui/app/components/provider-page-container/index.scss
  14. 1
      ui/app/components/provider-page-container/provider-page-container-content/index.js
  15. 71
      ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.component.js
  16. 11
      ui/app/components/provider-page-container/provider-page-container-content/provider-page-container-content.container.js
  17. 1
      ui/app/components/provider-page-container/provider-page-container-header/index.js
  18. 12
      ui/app/components/provider-page-container/provider-page-container-header/provider-page-container-header.component.js
  19. 50
      ui/app/components/provider-page-container/provider-page-container.component.js

@ -32,14 +32,11 @@
"reject": { "reject": {
"message": "Reject" "message": "Reject"
}, },
"providerAPIRequest": { "providerRequest": {
"message": "Ethereum API Request" "message": "$1 would like to connect to your account"
},
"reviewProviderRequest": {
"message": "Please review this Ethereum API request."
}, },
"providerRequestInfo": { "providerRequestInfo": {
"message": "The domain listed below is requesting access to the Ethereum blockchain and to view your current account. Always double check that you're on the correct site before approving access." "message": "This site is requesting access to view your current account address. Always make sure you trust the sites you interact with."
}, },
"accept": { "accept": {
"message": "Accept" "message": "Accept"
@ -212,6 +209,9 @@
"connect": { "connect": {
"message": "Connect" "message": "Connect"
}, },
"connectRequest": {
"message": "Connect Request"
},
"connecting": { "connecting": {
"message": "Connecting..." "message": "Connecting..."
}, },

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<path d="M34.5 19C34.5 27.0081 28.0081 33.5 20 33.5C11.9919 33.5 5.5 27.0081 5.5 19C5.5 10.9919 11.9919 4.5 20 4.5C28.0081 4.5 34.5 10.9919 34.5 19Z" fill="#61BA00" stroke="#61BA00"/>
<path d="M13.2998 19.7195L16.813 23.3195L26.1998 14.0195" stroke="white" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d" x="0" y="0" width="40" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="2.5"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -126,6 +126,8 @@ function listenForProviderRequest () {
extension.runtime.sendMessage({ extension.runtime.sendMessage({
action: 'init-provider-request', action: 'init-provider-request',
origin: source.location.hostname, origin: source.location.hostname,
siteImage: getSiteIcon(source),
siteTitle: getSiteName(source),
}) })
break break
case 'ETHEREUM_IS_APPROVED': case 'ETHEREUM_IS_APPROVED':
@ -285,3 +287,31 @@ function redirectToPhishingWarning () {
href: window.location.href, href: window.location.href,
})}` })}`
} }
function getSiteName (window) {
const document = window.document
const siteName = document.querySelector('head > meta[property="og:site_name"]')
if (siteName) {
return siteName.content
}
return document.title
}
function getSiteIcon (window) {
const document = window.document
// Use the site's favicon if it exists
const shortcutIcon = document.querySelector('head > link[rel="shortcut icon"]')
if (shortcutIcon) {
return shortcutIcon.href
}
// Search through available icons in no particular order
const icon = Array.from(document.querySelectorAll('head > link[rel="icon"]')).find((icon) => Boolean(icon.href))
if (icon) {
return icon.href
}
return null
}

@ -24,10 +24,11 @@ class ProviderApprovalController {
this.publicConfigStore = publicConfigStore this.publicConfigStore = publicConfigStore
this.store = new ObservableStore() this.store = new ObservableStore()
platform && platform.addMessageListener && platform.addMessageListener(({ action = '', origin }) => { if (platform && platform.addMessageListener) {
platform.addMessageListener(({ action = '', origin, siteTitle, siteImage }) => {
switch (action) { switch (action) {
case 'init-provider-request': case 'init-provider-request':
this._handleProviderRequest(origin) this._handleProviderRequest(origin, siteTitle, siteImage)
break break
case 'init-is-approved': case 'init-is-approved':
this._handleIsApproved(origin) this._handleIsApproved(origin)
@ -41,14 +42,17 @@ class ProviderApprovalController {
} }
}) })
} }
}
/** /**
* Called when a tab requests access to a full Ethereum provider API * Called when a tab requests access to a full Ethereum provider API
* *
* @param {string} origin - Origin of the window requesting full provider access * @param {string} origin - Origin of the window requesting full provider access
* @param {string} siteTitle - The title of the document requesting full provider access
* @param {string} siteImage - The icon of the window requesting full provider access
*/ */
_handleProviderRequest (origin) { _handleProviderRequest (origin, siteTitle, siteImage) {
this.store.updateState({ providerRequests: [{ origin }] }) this.store.updateState({ providerRequests: [{ origin, siteTitle, siteImage }] })
const isUnlocked = this.keyringController.memStore.getState().isUnlocked const isUnlocked = this.keyringController.memStore.getState().isUnlocked
if (this.isApproved(origin) && this.caching && isUnlocked) { if (this.isApproved(origin) && this.caching && isUnlocked) {
this.approveProviderRequest(origin) this.approveProviderRequest(origin)

@ -459,7 +459,7 @@ describe('MetaMask', function () {
dapp = windowHandles.find(handle => handle !== extension && handle !== popup) dapp = windowHandles.find(handle => handle !== extension && handle !== popup)
await delay(regularDelayMs) await delay(regularDelayMs)
const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Approve')]`), 10000) const approveButton = await findElement(driver, By.xpath(`//button[contains(text(), 'Connect')]`))
await approveButton.click() await approveButton.click()
}) })

@ -32,6 +32,8 @@
@import './pages/index'; @import './pages/index';
@import './provider-page-container/index';
@import './selected-account/index'; @import './selected-account/index';
@import './sender-to-recipient/index'; @import './sender-to-recipient/index';

@ -3,11 +3,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
background-color: lighten(rgb(125, 128, 130), 45%);
padding: 0 10px; padding: 0 10px;
border-radius: 4px; border-radius: 4px;
height: 25px; height: 25px;
&--colored {
background-color: lighten(rgb(125, 128, 130), 45%);
}
&--mainnet { &--mainnet {
background-color: lighten($blue-lagoon, 68%); background-color: lighten($blue-lagoon, 68%);
} }

@ -16,7 +16,12 @@ const networkToClassHash = {
} }
export default class NetworkDisplay extends Component { export default class NetworkDisplay extends Component {
static defaultProps = {
colored: true,
}
static propTypes = { static propTypes = {
colored: PropTypes.bool,
network: PropTypes.string, network: PropTypes.string,
provider: PropTypes.object, provider: PropTypes.object,
} }
@ -41,14 +46,16 @@ export default class NetworkDisplay extends Component {
} }
render () { render () {
const { network, provider: { type, nickname } } = this.props const { colored, network, provider: { type, nickname } } = this.props
const networkClass = networkToClassHash[network] const networkClass = networkToClassHash[network]
return ( return (
<div className={classnames( <div
'network-display__container', className={classnames('network-display__container', {
networkClass && ('network-display__container--' + networkClass) 'network-display__container--colored': colored,
)}> ['network-display__container--' + networkClass]: colored && networkClass,
})}
>
{ {
networkClass networkClass
? <div className={`network-display__icon network-display__icon--${networkClass}`} /> ? <div className={`network-display__icon network-display__icon--${networkClass}`} />

@ -67,7 +67,9 @@ export default class Home extends PureComponent {
} }
if (providerRequests && providerRequests.length > 0) { if (providerRequests && providerRequests.length > 0) {
return <ProviderApproval origin={providerRequests[0].origin} /> return (
<ProviderApproval providerRequest={providerRequests[0]} />
)
} }
return ( return (

@ -1,12 +1,12 @@
import PageContainerContent from '../../page-container'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React, { Component } from 'react' import React, { Component } from 'react'
import ProviderPageContainer from '../../provider-page-container'
export default class ProviderApproval extends Component { export default class ProviderApproval extends Component {
static propTypes = { static propTypes = {
approveProviderRequest: PropTypes.func, approveProviderRequest: PropTypes.func.isRequired,
origin: PropTypes.string, providerRequest: PropTypes.object.isRequired,
rejectProviderRequest: PropTypes.func, rejectProviderRequest: PropTypes.func.isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -14,22 +14,15 @@ export default class ProviderApproval extends Component {
}; };
render () { render () {
const { approveProviderRequest, origin, rejectProviderRequest } = this.props const { approveProviderRequest, providerRequest, rejectProviderRequest } = this.props
return ( return (
<PageContainerContent <ProviderPageContainer
title={this.context.t('providerAPIRequest')} approveProviderRequest={approveProviderRequest}
subtitle={this.context.t('reviewProviderRequest')} origin={providerRequest.origin}
contentComponent={( rejectProviderRequest={rejectProviderRequest}
<div className="provider_approval_content"> siteImage={providerRequest.siteImage}
{this.context.t('providerRequestInfo')} siteTitle={providerRequest.siteTitle}
<div className="provider_approval_origin">{origin}</div> />
</div>
)}
submitText={this.context.t('approve')}
cancelText={this.context.t('reject')}
onSubmit={() => { approveProviderRequest(origin) }}
onCancel={() => { rejectProviderRequest(origin) }}
onClose={() => { rejectProviderRequest(origin) }} />
) )
} }
} }

@ -0,0 +1,3 @@
export {default} from './provider-page-container.component'
export {default as ProviderPageContainerContent} from './provider-page-container-content'
export {default as ProviderPageContainerHeader} from './provider-page-container-header'

@ -0,0 +1,120 @@
.provider-approval-container {
display: flex;
&__header {
display: flex;
flex-direction: column;
align-items: flex-end;
border-bottom: 1px solid $geyser;
padding: 9px;
}
&__content {
display: flex;
overflow-y: auto;
flex: 1;
flex-direction: column;
justify-content: space-between;
color: #7C808E;
h1, h2 {
color: #4A4A4A;
display: flex;
justify-content: center;
text-align: center;
}
h2 {
font-size: 16px;
line-height: 18px;
padding: 20px;
}
h1 {
font-size: 22px;
line-height: 26px;
padding: 20px;
}
p {
padding: 0 40px;
text-align: center;
font-size: 12px;
line-height: 18px;
}
a, a:hover {
color: $dodger-blue;
}
.provider-approval-visual {
display: flex;
flex-direction: row;
justify-content: space-evenly;
position: relative;
margin: 0 32px;
section {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
h1 {
font-size: 14px;
line-height: 18px;
padding: 8px 0 0;
}
h2 {
font-size: 10px;
line-height: 14px;
padding: 0;
color: #A2A4AC;
}
&__check {
width: 40px;
height: 40px;
background: white url("/images/provider-approval-check.svg") no-repeat;
margin-top: 14px;
}
&__identicon {
width: 64px;
height: 64px;
&--default {
background-color: lightgray;
width: 64px;
height: 64px;
border-radius: 32px;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
}
&:before {
border-top: 2px dashed #CDD1E4;
content: "";
margin: 0 auto;
position: absolute;
top: 32px;
left: 0;
bottom: 0;
right: 0;
width: 65%;
z-index: -1;
}
}
.secure-badge {
display: flex;
justify-content: center;
padding: 25px;
}
}
}

@ -0,0 +1 @@
export {default} from './provider-page-container-content.container'

@ -0,0 +1,71 @@
import PropTypes from 'prop-types'
import React, {PureComponent} from 'react'
import Identicon from '../../identicon'
export default class ProviderPageContainerContent extends PureComponent {
static propTypes = {
origin: PropTypes.string.isRequired,
selectedIdentity: PropTypes.string.isRequired,
siteImage: PropTypes.string,
siteTitle: PropTypes.string.isRequired,
}
static contextTypes = {
t: PropTypes.func,
};
renderConnectVisual = () => {
const { origin, selectedIdentity, siteImage, siteTitle } = this.props
return (
<div className="provider-approval-visual">
<section>
{siteImage ? (
<img
className="provider-approval-visual__identicon"
src={siteImage}
/>
) : (
<i className="provider-approval-visual__identicon--default">
{siteTitle.charAt(0).toUpperCase()}
</i>
)}
<h1>{siteTitle}</h1>
<h2>{origin}</h2>
</section>
<span className="provider-approval-visual__check" />
<section>
<Identicon
className="provider-approval-visual__identicon"
address={selectedIdentity.address}
diameter={64}
/>
<h1>{selectedIdentity.name}</h1>
</section>
</div>
)
}
render () {
const { siteTitle } = this.props
const { t } = this.context
return (
<div className="provider-approval-container__content">
<section>
<h2>{t('connectRequest')}</h2>
{this.renderConnectVisual()}
<h1>{t('providerRequest', [siteTitle])}</h1>
<p>
{t('providerRequestInfo')}
<br/>
<a href="#">{t('learnMore')}.</a>
</p>
</section>
<section className="secure-badge">
<img src="/images/mm-secure.svg" />
</section>
</div>
)
}
}

@ -0,0 +1,11 @@
import { connect } from 'react-redux'
import ProviderPageContainerContent from './provider-page-container-content.component'
import { getSelectedIdentity } from '../../../selectors'
const mapStateToProps = (state) => {
return {
selectedIdentity: getSelectedIdentity(state),
}
}
export default connect(mapStateToProps)(ProviderPageContainerContent)

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

@ -0,0 +1,12 @@
import React, {PureComponent} from 'react'
import NetworkDisplay from '../../network-display'
export default class ProviderPageContainerHeader extends PureComponent {
render () {
return (
<div className="provider-approval-container__header">
<NetworkDisplay colored={false} />
</div>
)
}
}

@ -0,0 +1,50 @@
import PropTypes from 'prop-types'
import React, {PureComponent} from 'react'
import { ProviderPageContainerContent, ProviderPageContainerHeader } from './'
import { PageContainerFooter } from '../page-container'
export default class ProviderPageContainer extends PureComponent {
static propTypes = {
approveProviderRequest: PropTypes.func.isRequired,
origin: PropTypes.string.isRequired,
rejectProviderRequest: PropTypes.func.isRequired,
siteImage: PropTypes.string,
siteTitle: PropTypes.string.isRequired,
};
static contextTypes = {
t: PropTypes.func,
};
onCancel = () => {
const { origin, rejectProviderRequest } = this.props
rejectProviderRequest(origin)
}
onSubmit = () => {
const { approveProviderRequest, origin } = this.props
approveProviderRequest(origin)
}
render () {
const {origin, siteImage, siteTitle} = this.props
return (
<div className="page-container provider-approval-container">
<ProviderPageContainerHeader />
<ProviderPageContainerContent
origin={origin}
siteImage={siteImage}
siteTitle={siteTitle}
/>
<PageContainerFooter
onCancel={() => this.onCancel()}
cancelText={this.context.t('cancel')}
onSubmit={() => this.onSubmit()}
submitText={this.context.t('connect')}
submitButtonType="confirm"
/>
</div>
)
}
}
Loading…
Cancel
Save