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",
"@oceanprotocol/typographies": "^0.1.0",
"@reach/router": "^1.2.1",
"ethereum-address": "0.0.4",
"ms": "^2.1.1",
"react": "^16.8.6",
"react-blockies": "^1.4.1",

View File

@ -1,5 +1,3 @@
@import '../node_modules/@oceanprotocol/typographies/css/ocean-typo.css';
*,
*::before,
*::after {
@ -54,23 +52,16 @@ html.fullscreen {
h1,
h2,
h3,
h4 {
font-family: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif;
font-weight: 600;
}
button {
font-family: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Helvetica, Arial, sans-serif;
font-weight: 600;
h4,
h5 {
font-weight: 700;
}
.app {
margin-top: 35px;
padding: 5% 7%;
cursor: default;
height: 100vh;
height: calc(100vh - 5%);
transition: .15s ease-out;
width: 100%;
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 { AppContext } from '../store/createContext'
import { locale } from '../util/moneyFormatter'
import { formatCurrency } from '@coingecko/cryptoformat'
import './Balance.css'
const Balance = ({ balance }) => (
<h1 className="number">
<AppContext.Consumer>
{({ currency }) =>
formatCurrency(balance[currency], currency.toUpperCase(), locale)
export default class Balance extends PureComponent {
static contextType = AppContext
static propTypes = {
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(/ETH/, 'Ξ')
.replace(/OCEAN/, 'Ọ')
}
</AppContext.Consumer>
</h1>
)
Balance.propTypes = {
balance: PropTypes.object.isRequired
.replace(/OCEAN/, 'Ọ')}
</h1>
)
}
}
export default Balance

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { PureComponent } from 'react'
import { AppContext } from '../store/createContext'
import Balance from './Balance'
import { prices } from '../../config'
@ -19,26 +19,26 @@ const calculateTotalBalance = (accounts, currency) => {
return balanceTotal
}
const Total = () => (
<div className="number-unit number-unit--main">
<AppContext.Consumer>
{({ accounts }) => {
const conversions = Object.assign(
...prices.map(key => ({
[key]: calculateTotalBalance(accounts, key)
}))
)
export default class Total extends PureComponent {
static contextType = AppContext
const balanceNew = {
ocean: calculateTotalBalance(accounts, 'ocean'),
...conversions
}
render() {
const conversions = Object.assign(
...prices.map(key => ({
[key]: calculateTotalBalance(this.context.accounts, key)
}))
)
return <Balance balance={balanceNew} />
}}
</AppContext.Consumer>
<span className="label">Total Balance</span>
</div>
)
const balanceNew = {
ocean: calculateTotalBalance(this.context.accounts, 'ocean'),
...conversions
}
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;
}
.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 {
min-height: 55px;
}
@ -102,16 +85,6 @@
font-size: 2.5rem;
}
@keyframes updated {
0% {
background: rgba(255, 255, 255, .2);
}
100% {
background: rgba(255, 255, 255, 0);
}
}
@keyframes fadeIn {
0% {
opacity: 0;

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import ms from 'ms'
import Store from 'electron-store'
import { AppContext } from './createContext'
import fetchData from '../util/fetch'
import { refreshInterval, prices, oceanTokenContract } from '../../config'
export default class AppProvider extends PureComponent {
@ -19,24 +20,19 @@ export default class AppProvider extends PureComponent {
needsConfig: false,
prices: Object.assign(...prices.map(key => ({ [key]: 0 }))),
toggleCurrencies: currency => this.setState({ currency }),
setBalances: account => this.setBalances(account)
setBalances: () => this.setBalances()
}
async componentDidMount() {
const { accountsPref } = await this.getAccounts()
await this.fetchAndSetPrices()
await this.setBalances(accountsPref)
await this.setBalances()
await setInterval(this.fetchAndSetPrices, ms(refreshInterval))
await setInterval(this.setBalances, ms(refreshInterval))
setInterval(this.fetchAndSetPrices, ms(refreshInterval))
setInterval(this.setBalances, ms(refreshInterval))
this.setState({ isLoading: false })
}
componentWillUnmount() {
this.clearAccounts()
}
getAccounts() {
let accountsPref
@ -50,64 +46,41 @@ export default class AppProvider extends PureComponent {
accountsPref = []
}
return { accountsPref }
return accountsPref
}
clearAccounts() {
this.setState({ accounts: [] })
}
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(
async getBalance(account) {
const json = await fetchData(
`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
}
async fetchAndSetPrices() {
fetchAndSetPrices = async () => {
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}`
)
await this.setState({
prices: Object.assign(
...prices.map(key => ({
ocean: 1,
[key]: json['ocean-protocol'][key]
}))
)
})
const newPrizes = Object.assign(
...prices.map(key => ({
ocean: 1,
[key]: json['ocean-protocol'][key]
}))
)
this.setState({ prices: newPrizes })
}
setBalances(accounts) {
// TODO: make this less lazy and update numbers in place
// when they are changed instead of resetting all to 0 here
this.clearAccounts()
setBalances = async () => {
const accountsPref = await this.getAccounts()
const accountsArray = accounts ? accounts : this.state.accounts
let newAccounts = []
accountsArray.map(async account => {
const oceanBalance = await this.fetchBalance(account)
for (const account of accountsPref) {
const oceanBalance = await this.getBalance(account)
const conversions = Object.assign(
...prices.map(key => ({
@ -118,15 +91,17 @@ export default class AppProvider extends PureComponent {
const newAccount = {
address: account,
balance: {
ocean: oceanBalance || 0,
ocean: oceanBalance,
...conversions
}
}
await this.setState(prevState => ({
accounts: [...prevState.accounts, newAccount]
}))
})
newAccounts.push(newAccount)
}
if (newAccounts !== this.state.accounts) {
this.setState({ accounts: newAccounts })
}
}
render() {

View File

@ -1,5 +1,5 @@
import { createContext } from 'react'
const AppContext = createContext({})
const AppContext = createContext()
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