balance data fetching refactor, preferences tweaks

This commit is contained in:
Matthias Kretschmann 2019-05-09 23:28:58 +02:00
parent 96862db1c1
commit 057f27a970
Signed by: m
GPG Key ID: 606EEEF3C479A91F
11 changed files with 192 additions and 160 deletions

View File

@ -21,6 +21,7 @@
"@coingecko/cryptoformat": "^0.3.1", "@coingecko/cryptoformat": "^0.3.1",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@reach/router": "^1.2.1", "@reach/router": "^1.2.1",
"ethereum-address": "0.0.4",
"ms": "^2.1.1", "ms": "^2.1.1",
"react": "^16.8.6", "react": "^16.8.6",
"react-blockies": "^1.4.1", "react-blockies": "^1.4.1",

View File

@ -1,5 +1,3 @@
@import '../node_modules/@oceanprotocol/typographies/css/ocean-typo.css';
*, *,
*::before, *::before,
*::after { *::after {
@ -54,23 +52,16 @@ html.fullscreen {
h1, h1,
h2, h2,
h3, h3,
h4 { h4,
font-family: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont, h5 {
'Segoe UI', Helvetica, Arial, sans-serif; font-weight: 700;
font-weight: 600;
}
button {
font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Helvetica, Arial, sans-serif;
font-weight: 600;
} }
.app { .app {
margin-top: 35px; margin-top: 35px;
padding: 5% 7%; padding: 5% 7%;
cursor: default; cursor: default;
height: 100vh; height: calc(100vh - 5%);
transition: .15s ease-out; transition: .15s ease-out;
width: 100%; width: 100%;
overflow-y: auto; overflow-y: auto;

View File

@ -0,0 +1,35 @@
.number {
margin: 0;
transition: .15s ease-out;
-webkit-app-region: no-drag;
-webkit-user-select: text;
font-size: 1rem;
display: inline-block;
padding: 0 .3rem;
animation: fadeIn .5s ease-out;
border-radius: 4px;
}
.updated {
animation: updated .5s ease-out;
}
@keyframes updated {
0% {
background: rgba(255, 255, 255, .2);
}
100% {
background: rgba(255, 255, 255, 0);
}
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View File

@ -1,24 +1,28 @@
import React from 'react' import React, { PureComponent } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { AppContext } from '../store/createContext' import { AppContext } from '../store/createContext'
import { locale } from '../util/moneyFormatter' import { locale } from '../util/moneyFormatter'
import { formatCurrency } from '@coingecko/cryptoformat' import { formatCurrency } from '@coingecko/cryptoformat'
import './Balance.css'
const Balance = ({ balance }) => ( export default class Balance extends PureComponent {
<h1 className="number"> static contextType = AppContext
<AppContext.Consumer>
{({ currency }) => static propTypes = {
formatCurrency(balance[currency], currency.toUpperCase(), locale) balance: PropTypes.object.isRequired
}
render() {
const { currency } = this.context
const { balance } = this.props
return (
<h1 className="number">
{formatCurrency(balance[currency], currency.toUpperCase(), locale)
.replace(/BTC/, 'Ƀ') .replace(/BTC/, 'Ƀ')
.replace(/ETH/, 'Ξ') .replace(/ETH/, 'Ξ')
.replace(/OCEAN/, 'Ọ') .replace(/OCEAN/, 'Ọ')}
} </h1>
</AppContext.Consumer> )
</h1> }
)
Balance.propTypes = {
balance: PropTypes.object.isRequired
} }
export default Balance

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { PureComponent } from 'react'
import { AppContext } from '../store/createContext' import { AppContext } from '../store/createContext'
import Balance from './Balance' import Balance from './Balance'
import { prices } from '../../config' import { prices } from '../../config'
@ -19,26 +19,26 @@ const calculateTotalBalance = (accounts, currency) => {
return balanceTotal return balanceTotal
} }
const Total = () => ( export default class Total extends PureComponent {
<div className="number-unit number-unit--main"> static contextType = AppContext
<AppContext.Consumer>
{({ accounts }) => {
const conversions = Object.assign(
...prices.map(key => ({
[key]: calculateTotalBalance(accounts, key)
}))
)
const balanceNew = { render() {
ocean: calculateTotalBalance(accounts, 'ocean'), const conversions = Object.assign(
...conversions ...prices.map(key => ({
} [key]: calculateTotalBalance(this.context.accounts, key)
}))
)
return <Balance balance={balanceNew} /> const balanceNew = {
}} ocean: calculateTotalBalance(this.context.accounts, 'ocean'),
</AppContext.Consumer> ...conversions
<span className="label">Total Balance</span> }
</div>
)
export default Total return (
<div className="number-unit number-unit--main">
<Balance balance={balanceNew} />
<span className="label">Total Balance</span>
</div>
)
}
}

View File

@ -64,23 +64,6 @@
color: #f6388a; color: #f6388a;
} }
.number {
margin: 0;
transition: .15s ease-out;
font-weight: 400;
-webkit-app-region: no-drag;
-webkit-user-select: text;
font-size: 1rem;
display: inline-block;
padding: 0 .3rem;
animation: fadeIn .5s ease-out;
border-radius: 4px;
}
.updated {
animation: updated .5s ease-out;
}
.number-unit-wrap--accounts { .number-unit-wrap--accounts {
min-height: 55px; min-height: 55px;
} }
@ -102,16 +85,6 @@
font-size: 2.5rem; font-size: 2.5rem;
} }
@keyframes updated {
0% {
background: rgba(255, 255, 255, .2);
}
100% {
background: rgba(255, 255, 255, 0);
}
}
@keyframes fadeIn { @keyframes fadeIn {
0% { 0% {
opacity: 0; opacity: 0;

View File

@ -7,17 +7,14 @@
} }
.preferences__title { .preferences__title {
font-size: 2rem; font-size: 2.2rem;
margin-top: -1rem; margin-top: -1rem;
margin-bottom: 3rem; margin-bottom: 3rem;
} }
.preferences__close { .preferences__close {
text-decoration: none; text-decoration: none;
font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', font-size: 2rem;
Helvetica, Arial, sans-serif;
font-weight: 600;
font-size: 2.5rem;
position: absolute; position: absolute;
top: -1.5rem; top: -1.5rem;
right: 0; right: 0;
@ -37,13 +34,17 @@
border-top-color: #303030; border-top-color: #303030;
} }
.preference__list li { .preference__list li,
list-style: none; .preference__list li > div {
display: flex; display: flex;
align-items: center; align-items: center;
}
.preference__list li {
list-style: none;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid #e2e2e2; border-bottom: 1px solid #e2e2e2;
padding-top: .3rem; padding-top: .25rem;
padding-bottom: .25rem; padding-bottom: .25rem;
} }
@ -56,14 +57,16 @@
border: 0; border: 0;
box-shadow: none; box-shadow: none;
margin: 0; margin: 0;
padding: 0;
outline: 0; outline: 0;
color: #f6388a; color: #f6388a;
font-size: 1rem; font-size: 1rem;
text-transform: uppercase;
} }
button.delete { button.delete {
position: relative;
font-size: 2rem; font-size: 2rem;
top: -.2rem;
color: #41474e; color: #41474e;
transition: color .5s ease-out; transition: color .5s ease-out;
} }
@ -77,18 +80,27 @@ button.delete:hover {
-webkit-user-select: text; -webkit-user-select: text;
} }
.preference__title,
.preference__help {
display: inline-block;
margin-top: 0;
margin-bottom: .5rem;
}
.preference__title { .preference__title {
font-size: 1rem; font-size: 1.2rem;
}
.preference__help {
color: #8b98a9; color: #8b98a9;
margin-left: .5rem;
} }
.preference .identicon { .preference .identicon {
width: 1.5rem !important; width: 1.5rem !important;
height: 1.5rem !important; height: 1.5rem !important;
border-radius: 50%; border-radius: 50%;
vertical-align: middle; margin-right: .75rem;
margin-top: -.2rem;
margin-right: .5rem;
} }
.preference__input { .preference__input {
@ -105,3 +117,7 @@ button.delete:hover {
.dark .preference__input { .dark .preference__input {
color: #fff; color: #fff;
} }
.preference__error {
font-size: .9rem;
}

View File

@ -2,15 +2,16 @@ import React, { PureComponent } from 'react'
import { Link } from '@reach/router' import { Link } from '@reach/router'
import Store from 'electron-store' import Store from 'electron-store'
import Blockies from 'react-blockies' import Blockies from 'react-blockies'
import './Preferences.css' import ethereum_address from 'ethereum-address'
import { AppContext } from '../store/createContext' import { AppContext } from '../store/createContext'
import './Preferences.css'
export default class Preferences extends PureComponent { export default class Preferences extends PureComponent {
static contextType = AppContext static contextType = AppContext
store = new Store() store = new Store()
state = { accounts: [], input: '' } state = { accounts: [], input: '', error: '' }
componentDidMount() { componentDidMount() {
if (this.store.has('accounts')) { if (this.store.has('accounts')) {
@ -25,15 +26,27 @@ export default class Preferences extends PureComponent {
handleSave = e => { handleSave = e => {
e.preventDefault() e.preventDefault()
if ( const { accounts, input } = this.state
this.state.input !== '' &&
!this.state.accounts.includes(this.state.input) // duplication check const isEmpty = input === ''
) { const isDuplicate = accounts.includes(input)
const joined = [...this.state.accounts, this.state.input] const isAddress = ethereum_address.isAddress(input)
if (isEmpty) {
this.setState({ error: 'Please enter an address.' })
return
} else if (isDuplicate) {
this.setState({ error: 'Address already added. Try another one.' })
return
} else if (!isAddress) {
this.setState({ error: 'Not an Ethereum address. Try another one.' })
return
} else {
const joined = [...accounts, input]
this.store.set('accounts', joined) this.store.set('accounts', joined)
this.setState({ accounts: joined, input: '' }) this.setState({ accounts: joined, input: '', error: '' })
this.context.setBalances(joined) this.context.setBalances()
} }
} }
@ -50,10 +63,12 @@ export default class Preferences extends PureComponent {
this.store.set('accounts', array) this.store.set('accounts', array)
this.setState({ accounts: array }) this.setState({ accounts: array })
this.context.setBalances(array) this.context.setBalances()
} }
render() { render() {
const { accounts, input, error } = this.state
return ( return (
<div className="preferences"> <div className="preferences">
<h1 className="preferences__title">Preferences</h1>{' '} <h1 className="preferences__title">Preferences</h1>{' '}
@ -62,9 +77,12 @@ export default class Preferences extends PureComponent {
</Link> </Link>
<div className="preference"> <div className="preference">
<h2 className="preference__title">Accounts</h2> <h2 className="preference__title">Accounts</h2>
<p className="preference__help">
Add Ethereum account addresses holding Ocean Tokens.
</p>
<ul className="preference__list"> <ul className="preference__list">
{this.state.accounts && {accounts &&
this.state.accounts.map(account => ( accounts.map(account => (
<li key={account}> <li key={account}>
<div> <div>
<Blockies seed={account} size={10} scale={3} /> <Blockies seed={account} size={10} scale={3} />
@ -84,7 +102,7 @@ export default class Preferences extends PureComponent {
<input <input
type="text" type="text"
placeholder="0xxxxxxxx" placeholder="0xxxxxxxx"
value={this.state.input} value={input}
onChange={this.handleInputChange} onChange={this.handleInputChange}
className="preference__input" className="preference__input"
/> />
@ -96,6 +114,7 @@ export default class Preferences extends PureComponent {
</button> </button>
</li> </li>
</ul> </ul>
{error !== '' && <div className="preference__error">{error}</div>}
</div> </div>
</div> </div>
) )

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import ms from 'ms' import ms from 'ms'
import Store from 'electron-store' import Store from 'electron-store'
import { AppContext } from './createContext' import { AppContext } from './createContext'
import fetchData from '../util/fetch'
import { refreshInterval, prices, oceanTokenContract } from '../../config' import { refreshInterval, prices, oceanTokenContract } from '../../config'
export default class AppProvider extends PureComponent { export default class AppProvider extends PureComponent {
@ -19,24 +20,19 @@ export default class AppProvider extends PureComponent {
needsConfig: false, needsConfig: false,
prices: Object.assign(...prices.map(key => ({ [key]: 0 }))), prices: Object.assign(...prices.map(key => ({ [key]: 0 }))),
toggleCurrencies: currency => this.setState({ currency }), toggleCurrencies: currency => this.setState({ currency }),
setBalances: account => this.setBalances(account) setBalances: () => this.setBalances()
} }
async componentDidMount() { async componentDidMount() {
const { accountsPref } = await this.getAccounts()
await this.fetchAndSetPrices() await this.fetchAndSetPrices()
await this.setBalances(accountsPref) await this.setBalances()
await setInterval(this.fetchAndSetPrices, ms(refreshInterval)) setInterval(this.fetchAndSetPrices, ms(refreshInterval))
await setInterval(this.setBalances, ms(refreshInterval)) setInterval(this.setBalances, ms(refreshInterval))
this.setState({ isLoading: false }) this.setState({ isLoading: false })
} }
componentWillUnmount() {
this.clearAccounts()
}
getAccounts() { getAccounts() {
let accountsPref let accountsPref
@ -50,64 +46,41 @@ export default class AppProvider extends PureComponent {
accountsPref = [] accountsPref = []
} }
return { accountsPref } return accountsPref
} }
clearAccounts() { async getBalance(account) {
this.setState({ accounts: [] }) const json = await fetchData(
}
async fetch(url) {
try {
const response = await fetch(url)
if (response.status !== 200) {
return console.log('Non-200 response: ' + response.status) // eslint-disable-line
}
const json = await response.json()
if (!json) return
return json
} catch (error) {
console.log('Error parsing json:' + error) // eslint-disable-line
}
}
async fetchBalance(account) {
const json = await this.fetch(
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest` `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest`
) )
const balance = (json.result /= 1000000000000000000) // Convert from wei 10^18 const balance = (json.result /= 1000000000000000000) // Convert from vodka 10^18
return balance return balance
} }
async fetchAndSetPrices() { fetchAndSetPrices = async () => {
const currencies = prices.join(',') const currencies = prices.join(',')
const json = await this.fetch( const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}` `https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}`
) )
await this.setState({ const newPrizes = Object.assign(
prices: Object.assign( ...prices.map(key => ({
...prices.map(key => ({ ocean: 1,
ocean: 1, [key]: json['ocean-protocol'][key]
[key]: json['ocean-protocol'][key] }))
})) )
)
}) this.setState({ prices: newPrizes })
} }
setBalances(accounts) { setBalances = async () => {
// TODO: make this less lazy and update numbers in place const accountsPref = await this.getAccounts()
// when they are changed instead of resetting all to 0 here
this.clearAccounts()
const accountsArray = accounts ? accounts : this.state.accounts let newAccounts = []
accountsArray.map(async account => { for (const account of accountsPref) {
const oceanBalance = await this.fetchBalance(account) const oceanBalance = await this.getBalance(account)
const conversions = Object.assign( const conversions = Object.assign(
...prices.map(key => ({ ...prices.map(key => ({
@ -118,15 +91,17 @@ export default class AppProvider extends PureComponent {
const newAccount = { const newAccount = {
address: account, address: account,
balance: { balance: {
ocean: oceanBalance || 0, ocean: oceanBalance,
...conversions ...conversions
} }
} }
await this.setState(prevState => ({ newAccounts.push(newAccount)
accounts: [...prevState.accounts, newAccount] }
}))
}) if (newAccounts !== this.state.accounts) {
this.setState({ accounts: newAccounts })
}
} }
render() { render() {

View File

@ -1,5 +1,5 @@
import { createContext } from 'react' import { createContext } from 'react'
const AppContext = createContext({}) const AppContext = createContext()
export { AppContext } export { AppContext }

18
src/util/fetch.js Normal file
View File

@ -0,0 +1,18 @@
const fetchData = async url => {
try {
const response = await fetch(url)
if (response.status !== 200) {
return console.log('Non-200 response: ' + response.status) // eslint-disable-line
}
const json = await response.json()
if (!json) return
return json
} catch (error) {
console.log('Error parsing json:' + error) // eslint-disable-line
}
}
export default fetchData