1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02:00

Update settings screen to new UI

This commit is contained in:
Alexander Tseung 2017-10-22 22:40:03 -07:00
parent 8f3b762461
commit 2c032e0df4
10 changed files with 605 additions and 296 deletions

View File

@ -21,7 +21,7 @@ const generateLostAccountsNotice = require('../lib/lost-accounts-notice')
const WalletView = require('./components/wallet-view')
// other views
const ConfigScreen = require('./config')
const Settings = require('./settings')
const AddTokenScreen = require('./add-token')
const Import = require('./accounts/import')
const InfoScreen = require('./info')
@ -383,7 +383,7 @@ App.prototype.renderPrimary = function () {
case 'config':
log.debug('rendering config screen')
return h(ConfigScreen, {key: 'config'})
return h(Settings, {key: 'config'})
case 'import-menu':
log.debug('rendering import screen')

View File

@ -0,0 +1,91 @@
const { Component, PropTypes } = require('react')
const h = require('react-hyperscript')
const classnames = require('classnames')
const R = require('ramda')
class SimpleDropdown extends Component {
constructor (props) {
super(props)
this.state = {
isOpen: false,
}
}
getDisplayValue () {
const { selectedOption, options } = this.props
const matchesOption = option => option.value === selectedOption
const matchingOption = R.find(matchesOption)(options)
return matchingOption
? matchingOption.displayValue || matchingOption.value
: selectedOption
}
handleClose () {
this.setState({ isOpen: false })
}
toggleOpen () {
const { isOpen } = this.state
this.setState({ isOpen: !isOpen })
}
renderOptions () {
const { options, onSelect, selectedOption } = this.props
return h('div', [
h('div.simple-dropdown__close-area', {
onClick: event => {
event.stopPropagation()
this.handleClose()
},
}),
h('div.simple-dropdown__options', [
...options.map(option => {
return h(
'div.simple-dropdown__option',
{
className: classnames({
'simple-dropdown__option--selected': option.value === selectedOption,
}),
key: option.value,
onClick: () => {
if (option.value !== selectedOption) {
onSelect(option.value)
}
this.handleClose()
},
},
option.displayValue || option.value,
)
}),
]),
])
}
render () {
const { placeholder } = this.props
const { isOpen } = this.state
return h(
'div.simple-dropdown',
{
onClick: () => this.toggleOpen(),
},
[
h('div.simple-dropdown__selected', this.getDisplayValue() || placeholder || 'Select'),
h('i.fa.fa-caret-down.fa-lg.simple-dropdown__caret'),
isOpen && this.renderOptions(),
]
)
}
}
SimpleDropdown.propTypes = {
options: PropTypes.array.isRequired,
placeholder: PropTypes.string,
onSelect: PropTypes.func,
selectedOption: PropTypes.string,
}
module.exports = SimpleDropdown

View File

@ -1,37 +1,40 @@
const Component = require('react').Component
const { Component } = require('react')
const h = require('react-hyperscript')
const inherits = require('util').inherits
const classnames = require('classnames')
class TabBar extends Component {
constructor (props) {
super(props)
const { defaultTab, tabs } = props
this.state = {
subview: defaultTab || tabs[0].key,
}
}
render () {
const { tabs = [], tabSelected } = this.props
const { subview } = this.state
return (
h('.tab-bar', {}, [
tabs.map((tab) => {
const { key, content } = tab
return h('div', {
className: classnames('tab-bar__tab pointer', {
'tab-bar__tab--active': subview === key,
}),
onClick: () => {
this.setState({ subview: key })
tabSelected(key)
},
key,
}, content)
}),
h('div.tab-bar__tab.tab-bar__grow-tab'),
])
)
}
}
module.exports = TabBar
inherits(TabBar, Component)
function TabBar () {
Component.call(this)
}
TabBar.prototype.render = function () {
const props = this.props
const state = this.state || {}
const { tabs = [], defaultTab, tabSelected } = props
const { subview = defaultTab } = state
return (
h('.flex-row.space-around.text-transform-uppercase', {
style: {
background: '#EBEBEB',
color: '#AEAEAE',
paddingTop: '4px',
minHeight: '30px',
},
}, tabs.map((tab) => {
const { key, content } = tab
return h(subview === key ? '.activeForm' : '.inactiveForm.pointer', {
onClick: () => {
this.setState({ subview: key })
tabSelected(key)
},
}, content)
}))
)
}

View File

@ -1,215 +0,0 @@
const inherits = require('util').inherits
const Component = require('react').Component
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const actions = require('./actions')
const infuraCurrencies = require('./infura-conversion.json').objects.sort((a, b) => {
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
})
const validUrl = require('valid-url')
const exportAsFile = require('./util').exportAsFile
module.exports = connect(mapStateToProps)(ConfigScreen)
function mapStateToProps (state) {
return {
metamask: state.metamask,
warning: state.appState.warning,
}
}
inherits(ConfigScreen, Component)
function ConfigScreen () {
Component.call(this)
}
ConfigScreen.prototype.render = function () {
var state = this.props
var metamaskState = state.metamask
var warning = state.warning
return (
h('.flex-column.flex-grow', { style: { marginTop: '32px' } }, [
// subtitle and nav
h('.section-title.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: (event) => {
state.dispatch(actions.goHome())
},
}),
h('h2.page-subtitle', 'Settings'),
]),
h('.error', {
style: {
display: warning ? 'block' : 'none',
padding: '0 20px',
textAlign: 'center',
},
}, warning),
// conf view
h('.flex-column.flex-justify-center.flex-grow.select-none', [
h('.flex-space-around', {
style: {
padding: '20px',
},
}, [
currentProviderDisplay(metamaskState),
h('div', { style: {display: 'flex'} }, [
h('input#new_rpc', {
placeholder: 'New RPC URL',
style: {
width: 'inherit',
flex: '1 0 auto',
height: '30px',
margin: '8px',
},
onKeyPress (event) {
if (event.key === 'Enter') {
var element = event.target
var newRpc = element.value
rpcValidation(newRpc, state)
}
},
}),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
event.preventDefault()
var element = document.querySelector('input#new_rpc')
var newRpc = element.value
rpcValidation(newRpc, state)
},
}, 'Save'),
]),
h('hr.horizontal-line'),
currentConversionInformation(metamaskState, state),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('p', {
style: {
fontFamily: 'Montserrat Light',
fontSize: '13px',
},
}, `State logs contain your public account addresses and sent transactions.`),
h('br'),
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
exportAsFile('MetaMask State Logs', window.logState())
},
}, 'Download State Logs'),
]),
h('hr.horizontal-line'),
h('div', {
style: {
marginTop: '20px',
},
}, [
h('button', {
style: {
alignSelf: 'center',
},
onClick (event) {
event.preventDefault()
state.dispatch(actions.revealSeedConfirmation())
},
}, 'Reveal Seed Words'),
]),
]),
]),
])
)
}
function rpcValidation (newRpc, state) {
if (validUrl.isWebUri(newRpc)) {
state.dispatch(actions.setRpcTarget(newRpc))
} else {
var appendedRpc = `http://${newRpc}`
if (validUrl.isWebUri(appendedRpc)) {
state.dispatch(actions.displayWarning('URIs require the appropriate HTTP/HTTPS prefix.'))
} else {
state.dispatch(actions.displayWarning('Invalid RPC URI'))
}
}
}
function currentConversionInformation (metamaskState, state) {
var currentCurrency = metamaskState.currentCurrency
var conversionDate = metamaskState.conversionDate
return h('div', [
h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, 'Current Conversion'),
h('span', {style: { fontWeight: 'bold', paddingRight: '10px', fontSize: '13px'}}, `Updated ${Date(conversionDate)}`),
h('select#currentCurrency', {
onChange (event) {
event.preventDefault()
var element = document.getElementById('currentCurrency')
var newCurrency = element.value
state.dispatch(actions.setCurrentCurrency(newCurrency))
},
defaultValue: currentCurrency,
}, infuraCurrencies.map((currency) => {
console.log(`currency`, currency);
return h('option', {key: currency.quote.code, value: currency.quote.code}, `${currency.quote.code.toUpperCase()} - ${currency.quote.name}`)
})
),
])
}
function currentProviderDisplay (metamaskState) {
var provider = metamaskState.provider
var title, value
switch (provider.type) {
case 'mainnet':
title = 'Current Network'
value = 'Main Ethereum Network'
break
case 'ropsten':
title = 'Current Network'
value = 'Ropsten Test Network'
break
case 'kovan':
title = 'Current Network'
value = 'Kovan Test Network'
break
case 'rinkeby':
title = 'Current Network'
value = 'Rinkeby Test Network'
break
default:
title = 'Current RPC'
value = metamaskState.provider.rpcTarget
}
return h('div', [
h('span', {style: { fontWeight: 'bold', paddingRight: '10px'}}, title),
h('span', value),
])
}

View File

@ -38,3 +38,8 @@
@import './gas-slider.scss';
@import './settings.scss';
@import './tab-bar.scss';
@import './simple-dropdown.scss';

View File

@ -0,0 +1,135 @@
.settings {
position: relative;
background: $white;
display: flex;
flex-flow: column nowrap;
height: auto;
overflow: auto;
}
.settings__header {
padding: 25px;
}
.settings__close-button::after {
content: '\00D7';
font-size: 40px;
color: $dusty-gray;
position: absolute;
top: 25px;
right: 30px;
cursor: pointer;
}
.settings__error {
padding-bottom: 20px;
text-align: center;
color: $crimson;
}
.settings__content {
padding: 0 25px;
}
.settings__content-row {
display: flex;
flex-direction: row;
padding: 10px 0 20px;
@media screen and (max-width: 575px) {
flex-direction: column;
}
}
.settings__content-item {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
padding: 0 5px;
height: 71px;
}
.settings__content-item-col {
max-width: 300px;
display: flex;
flex-direction: column;
@media screen and (max-width: 575px) {
max-width: 100%;
width: 100%;
}
}
.settings__content-description {
font-size: 14px;
color: $dusty-gray;
padding-top: 5px;
}
.settings__input {
padding-left: 10px;
font-size: 14px;
height: 40px;
}
.settings__input::-webkit-input-placeholder {
font-weight: 100;
color: $dusty-gray;
}
.settings__input::-moz-placeholder {
font-weight: 100;
color: $dusty-gray;
}
.settings__input:-ms-input-placeholder {
font-weight: 100;
color: $dusty-gray;
}
.settings__input:-moz-placeholder {
font-weight: 100;
color: $dusty-gray;
}
.settings__provider-wrapper {
font-size: 16px;
border: 1px solid $alto;
border-radius: 2px;
padding: 15px;
background-color: $white;
display: flex;
align-items: center;
justify-content: flex-start;
}
.settings__provider-icon {
height: 10px;
width: 10px;
margin-right: 10px;
border-radius: 10px;
}
.settings__rpc-save-button {
align-self: flex-end;
padding: 5px;
text-transform: uppercase;
color: $dusty-gray;
cursor: pointer;
}
.settings__clear-button {
font-size: 16px;
border: 1px solid $curious-blue;
color: $curious-blue;
border-radius: 2px;
padding: 18px;
background-color: $white;
text-transform: uppercase;
}
.settings__clear-button--red {
border: 1px solid $monzo;
color: $monzo;
}

View File

@ -0,0 +1,65 @@
.simple-dropdown {
height: 56px;
display: flex;
justify-content: flex-start;
align-items: center;
border: 1px solid $alto;
border-radius: 4px;
background-color: $white;
font-size: 16px;
color: #4d4d4d;
cursor: pointer;
position: relative;
}
.simple-dropdown__caret {
color: $silver;
padding: 0 10px;
}
.simple-dropdown__selected {
flex-grow: 1;
padding: 0 15px;
}
.simple-dropdown__options {
z-index: 1050;
position: absolute;
height: 220px;
width: 100%;
border: 1px solid #d2d8dd;
border-radius: 4px;
background-color: #fff;
-webkit-box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .11);
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .11);
margin-top: 10px;
overflow-y: scroll;
left: 0;
top: 100%;
}
.simple-dropdown__option {
padding: 10px;
&:hover {
background-color: $gallery;
}
}
.simple-dropdown__option--selected {
background-color: $alto;
&:hover {
background-color: $alto;
cursor: default;
}
}
.simple-dropdown__close-area {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,23 @@
.tab-bar {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-end;
}
.tab-bar__tab {
min-width: 0;
flex: 0 0 auto;
padding: 15px 25px;
border-bottom: 1px solid $alto;
box-sizing: border-box;
font-size: 18px;
}
.tab-bar__tab--active {
border-color: $black;
}
.tab-bar__grow-tab {
flex-grow: 1;
}

View File

@ -3,7 +3,7 @@ const h = require('react-hyperscript')
const inherits = require('util').inherits
const AccountAndTransactionDetails = require('./account-and-transaction-details')
const HDRestoreVaultScreen = require('./keychains/hd/restore-vault')
const ConfigScreen = require('./config')
const Settings = require('./settings')
const UnlockScreen = require('./unlock')
module.exports = MainContainer
@ -38,7 +38,7 @@ MainContainer.prototype.render = function () {
case 'config':
log.debug('rendering config screen from unlock screen.')
contents = {
component: ConfigScreen,
component: Settings,
key: 'config',
}
break

View File

@ -1,59 +1,261 @@
const inherits = require('util').inherits
const Component = require('react').Component
const { Component } = require('react')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const { connect } = require('react-redux')
const actions = require('./actions')
const infuraCurrencies = require('./infura-conversion.json')
const validUrl = require('valid-url')
const { exportAsFile } = require('./util')
const TabBar = require('./components/tab-bar')
const SimpleDropdown = require('./components/dropdowns/simple-dropdown')
module.exports = connect(mapStateToProps)(AppSettingsPage)
const getInfuraCurrencyOptions = () => {
const sortedCurrencies = infuraCurrencies.objects.sort((a, b) => {
return a.quote.name.toLocaleLowerCase().localeCompare(b.quote.name.toLocaleLowerCase())
})
function mapStateToProps (state) {
return {}
return sortedCurrencies.map(({ quote: { code, name } }) => {
return {
displayValue: `${code.toUpperCase()} - ${name}`,
key: code,
value: code,
}
})
}
inherits(AppSettingsPage, Component)
function AppSettingsPage () {
Component.call(this)
}
class Settings extends Component {
constructor (args) {
super(args)
this.state = {
activeTab: 'settings',
newRpc: '',
}
}
AppSettingsPage.prototype.render = function () {
return (
h('.account-detail-section.flex-column.flex-grow', [
// subtitle and nav
h('.flex-row.flex-center', [
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
onClick: this.navigateToAccounts.bind(this),
}),
h('h2.page-subtitle', 'Settings'),
]),
h('label', {
htmlFor: 'settings-rpc-endpoint',
}, 'RPC Endpoint:'),
h('input', {
type: 'url',
id: 'settings-rpc-endpoint',
onKeyPress: this.onKeyPress.bind(this),
renderTabs () {
return h('div.settings__tabs', [
h(TabBar, {
tabs: [
{ content: 'Settings', key: 'settings' },
{ content: 'Info', key: 'info' },
],
defaultTab: 'settings',
tabSelected: key => this.setState({ activeTab: key }),
}),
])
}
)
}
renderCurrentConversion () {
const { metamask: { currentCurrency, conversionDate }, setCurrentCurrency } = this.props
AppSettingsPage.prototype.componentDidMount = function () {
document.querySelector('input').focus()
}
return h('div.settings__content-row', [
h('div.settings__content-item', [
h('span', 'Current Conversion'),
h('span.settings__content-description', `Updated ${Date(conversionDate)}`),
]),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h(SimpleDropdown, {
placeholder: 'Select Currency',
options: getInfuraCurrencyOptions(),
selectedOption: currentCurrency,
onSelect: newCurrency => setCurrentCurrency(newCurrency),
}),
]),
]),
])
}
AppSettingsPage.prototype.onKeyPress = function (event) {
// get submit event
if (event.key === 'Enter') {
// this.submitPassword(event)
renderCurrentProvider () {
const { metamask: { provider = {} } } = this.props
let title, value, color
switch (provider.type) {
case 'mainnet':
title = 'Current Network'
value = 'Main Ethereum Network'
color = '#038789'
break
case 'ropsten':
title = 'Current Network'
value = 'Ropsten Test Network'
color = '#e91550'
break
case 'kovan':
title = 'Current Network'
value = 'Kovan Test Network'
color = '#690496'
break
case 'rinkeby':
title = 'Current Network'
value = 'Rinkeby Test Network'
color = '#ebb33f'
break
default:
title = 'Current RPC'
value = provider.rpcTarget
}
return h('div.settings__content-row', [
h('div.settings__content-item', title),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('div.settings__provider-wrapper', [
h('div.settings__provider-icon', { style: { background: color } }),
h('div', value),
]),
]),
]),
])
}
renderNewRpcUrl () {
return (
h('div.settings__content-row', [
h('div.settings__content-item', [
h('span', 'New RPC URL'),
]),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('input.settings__input', {
placeholder: 'New RPC URL',
onChange: event => this.setState({ newRpc: event.target.value }),
onKeyPress: event => {
if (event.key === 'Enter') {
this.validateRpc(this.state.newRpc)
}
},
}),
h('div.settings__rpc-save-button', {
onClick: event => {
event.preventDefault()
this.validateRpc(this.state.newRpc)
},
}, 'Save'),
]),
]),
])
)
}
validateRpc (newRpc) {
const { setRpcTarget, displayWarning } = this.props
if (validUrl.isWebUri(newRpc)) {
setRpcTarget(newRpc)
} else {
const appendedRpc = `http://${newRpc}`
if (validUrl.isWebUri(appendedRpc)) {
displayWarning('URIs require the appropriate HTTP/HTTPS prefix.')
} else {
displayWarning('Invalid RPC URI')
}
}
}
renderStateLogs () {
return (
h('div.settings__content-row', [
h('div.settings__content-item', [
h('div', 'State Logs'),
h(
'div.settings__content-description',
'State logs contain your public account addresses and sent transactions.'
),
]),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('button.settings__clear-button', {
onClick (event) {
exportAsFile('MetaMask State Logs', window.logState())
},
}, 'Download State Logs'),
]),
]),
])
)
}
renderSeedWords () {
const { revealSeedConfirmation } = this.props
return (
h('div.settings__content-row', [
h('div.settings__content-item', 'Reveal Seed Words'),
h('div.settings__content-item', [
h('div.settings__content-item-col', [
h('button.settings__clear-button.settings__clear-button--red', {
onClick (event) {
event.preventDefault()
revealSeedConfirmation()
},
}, 'Reveal Seed Words'),
]),
]),
])
)
}
renderSettingsContent () {
const { warning } = this.props
return (
h('div.settings__content', [
warning && h('div.settings__error', warning),
this.renderCurrentConversion(),
this.renderCurrentProvider(),
this.renderNewRpcUrl(),
this.renderStateLogs(),
this.renderSeedWords(),
])
)
}
renderInfoContent () {
}
render () {
const { goHome } = this.props
const { activeTab } = this.state
return (
h('.main-container.settings', {}, [
h('.settings__header', [
h('div.settings__close-button', {
onClick: goHome,
}),
this.renderTabs(),
]),
activeTab === 'settings'
? this.renderSettingsContent()
: this.renderInfoContent(),
])
)
}
}
AppSettingsPage.prototype.navigateToAccounts = function (event) {
event.stopPropagation()
this.props.dispatch(actions.showAccountsPage())
const mapStateToProps = state => {
return {
metamask: state.metamask,
warning: state.appState.warning,
}
}
const mapDispatchToProps = dispatch => {
return {
goHome: () => dispatch(actions.goHome()),
setCurrentCurrency: currency => dispatch(actions.setCurrentCurrency(currency)),
setRpcTarget: newRpc => dispatch(actions.setRpcTarget(newRpc)),
displayWarning: warning => dispatch(actions.displayWarning(warning)),
revealSeedConfirmation: () => dispatch(actions.revealSeedConfirmation()),
}
}
module.exports = connect(mapStateToProps, mapDispatchToProps)(Settings)