1
0
mirror of https://github.com/kremalicious/blowfish.git synced 2024-11-22 09:47:00 +01:00

provider splitup and refactor

This commit is contained in:
Matthias Kretschmann 2020-02-25 04:10:06 +01:00
parent c3edae6280
commit 2efee0ec73
Signed by: m
GPG Key ID: 606EEEF3C479A91F
16 changed files with 213 additions and 144 deletions

View File

@ -30,11 +30,13 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@coingecko/cryptoformat": "^0.3.4", "@coingecko/cryptoformat": "^0.3.4",
"axios": "^0.19.2",
"electron-is-dev": "^1.1.0", "electron-is-dev": "^1.1.0",
"electron-next": "^3.1.5", "electron-next": "^3.1.5",
"electron-store": "^5.1.0", "electron-store": "^5.1.1",
"ethereum-address": "^0.0.4", "ethereum-address": "^0.0.4",
"ethereum-blockies": "github:MyEtherWallet/blockies", "ethereum-blockies": "github:MyEtherWallet/blockies",
"ethjs-unit": "^0.1.6",
"ms": "^2.1.2", "ms": "^2.1.2",
"shortid": "^2.2.15" "shortid": "^2.2.15"
}, },
@ -43,15 +45,15 @@
"@babel/preset-env": "^7.8.4", "@babel/preset-env": "^7.8.4",
"@jest-runner/electron": "^2.0.3", "@jest-runner/electron": "^2.0.3",
"@react-mock/state": "^0.1.8", "@react-mock/state": "^0.1.8",
"@svgr/webpack": "^5.1.0", "@svgr/webpack": "^5.2.0",
"@testing-library/jest-dom": "^5.1.1", "@testing-library/jest-dom": "^5.1.1",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.1",
"auto-changelog": "^1.16.2", "auto-changelog": "^1.16.2",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^25.1.0", "babel-jest": "^25.1.0",
"copy": "^0.3.2", "copy": "^0.3.2",
"cross-env": "^7.0.0", "cross-env": "^7.0.0",
"electron": "^8.0.0", "electron": "^8.0.1",
"electron-builder": "^22.3.2", "electron-builder": "^22.3.2",
"electron-devtools-installer": "^2.2.4", "electron-devtools-installer": "^2.2.4",
"eslint": "^6.8.0", "eslint": "^6.8.0",
@ -59,14 +61,14 @@
"eslint-plugin-react": "^7.18.3", "eslint-plugin-react": "^7.18.3",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"next": "^9.2.1", "next": "^9.2.2",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"prettier-stylelint": "^0.4.2", "prettier-stylelint": "^0.4.2",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-pose": "^4.0.10", "react-pose": "^4.0.10",
"release-it": "^12.4.3", "release-it": "^12.6.1",
"stylelint": "^13.1.0", "stylelint": "^13.2.0",
"stylelint-config-css-modules": "^2.2.0", "stylelint-config-css-modules": "^2.2.0",
"stylelint-config-standard": "^20.0.0" "stylelint-config-standard": "^20.0.0"
}, },

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose' import posed, { PoseGroup } from 'react-pose'
import shortid from 'shortid' import shortid from 'shortid'
import AppProvider from './store/AppProvider' import AppProvider from './store/AppProvider'
import PriceProvider from './store/PriceProvider'
import { defaultAnimation } from './components/Animations' import { defaultAnimation } from './components/Animations'
import Titlebar from './components/Titlebar' import Titlebar from './components/Titlebar'
import styles from './Layout.module.css' import styles from './Layout.module.css'
@ -11,14 +12,16 @@ const Animation = posed.div(defaultAnimation)
export default function Layout({ children }) { export default function Layout({ children }) {
return ( return (
<AppProvider> <PriceProvider>
{process.platform === 'darwin' && <Titlebar />} <AppProvider>
<div className={styles.app}> {process.platform === 'darwin' && <Titlebar />}
<PoseGroup animateOnMount> <div className={styles.app}>
<Animation key={shortid.generate()}>{children}</Animation> <PoseGroup animateOnMount>
</PoseGroup> <Animation key={shortid.generate()}>{children}</Animation>
</div> </PoseGroup>
</AppProvider> </div>
</AppProvider>
</PriceProvider>
) )
} }

View File

@ -1,7 +1,7 @@
import React, { useContext } from 'react' import React, { useContext } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import posed, { PoseGroup } from 'react-pose' import posed, { PoseGroup } from 'react-pose'
import { AppContext } from '../../store/createContext' import { AppContext, PriceContext } from '../../store/createContext'
import { cryptoFormatter } from '../../../utils' import { cryptoFormatter } from '../../../utils'
import stylesIndex from '../../pages/index.module.css' import stylesIndex from '../../pages/index.module.css'
import styles from './Ticker.module.css' import styles from './Ticker.module.css'
@ -10,7 +10,7 @@ import { fadeIn } from '../Animations'
const Item = posed.div(fadeIn) const Item = posed.div(fadeIn)
const Change = ({ currency }) => { const Change = ({ currency }) => {
const { priceChanges } = useContext(AppContext) const { priceChanges } = useContext(PriceContext)
const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-') const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-')
let classes = isNegative ? styles.negative : styles.positive let classes = isNegative ? styles.negative : styles.positive
@ -27,13 +27,10 @@ Change.propTypes = {
} }
const Items = () => { const Items = () => {
const { const { prices } = useContext(PriceContext)
prices, const { needsConfig, currency, toggleCurrencies, accentColor } = useContext(
needsConfig, AppContext
currency, )
toggleCurrencies,
accentColor
} = useContext(AppContext)
const activeStyle = { const activeStyle = {
backgroundColor: accentColor, backgroundColor: accentColor,

View File

@ -2,9 +2,9 @@ import React, { useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Router from 'next/router' import Router from 'next/router'
// import { ipcRenderer } from 'electron' // import { ipcRenderer } from 'electron'
import Layout from '../Layout'
import '../global.css' import '../global.css'
import Layout from '../Layout'
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
useEffect(() => { useEffect(() => {

View File

@ -1,120 +1,81 @@
import React, { PureComponent } from 'react' import React, { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import ms from 'ms' import ms from 'ms'
// import { ipcRenderer } from 'electron' // import { ipcRenderer } from 'electron'
import Store from 'electron-store' import Store from 'electron-store'
import { AppContext } from './createContext' import unit from 'ethjs-unit'
import { AppContext, PriceContext } from './createContext'
import { fetchData } from '../../utils' import { fetchData } from '../../utils'
import { refreshInterval, conversions, oceanTokenContract } from '../../config' import { refreshInterval, conversions, oceanTokenContract } from '../../config'
// construct initial prices Map to get consistent async function getBalance(account) {
// order for Ticker and Touchbar const json = await fetchData(
let pricesMap = new Map() `https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest`
pricesMap.set('ocean', 1) )
conversions.map(key => pricesMap.set(key, 0))
export default class AppProvider extends PureComponent { const balance = unit.fromWei(`${json.result}`, 'ether')
static propTypes = { return balance
children: PropTypes.any.isRequired }
}
store = process.env.NODE_ENV === 'test' ? new Store() : global.store export default function AppProvider({ children }) {
const { prices } = useContext(PriceContext)
const [isLoading, setIsLoading] = useState(true)
const [accounts, setAccounts] = useState([])
const [needsConfig, setNeedsConfig] = useState(false)
const [currency, setCurrency] = useState('ocean')
const [accentColor, setAccentColor] = useState('#f6388a')
state = { useEffect(() => {
isLoading: true,
accounts: [],
currency: 'ocean',
needsConfig: false,
prices: pricesMap,
priceChanges: Object.assign(
...conversions.map(key => ({
[key]: 0
}))
),
toggleCurrencies: currency => this.toggleCurrencies(currency),
setBalances: () => this.setBalances(),
accentColor: '#f6388a'
}
async componentDidMount() {
// listener for accent color // listener for accent color
global.ipcRenderer.on('accent-color', (evt, accentColor) => { global.ipcRenderer.on('accent-color', (evt, accentColor) => {
this.setState({ accentColor }) setAccentColor(accentColor)
}) })
}, [])
// listener for touchbar useEffect(() => {
global.ipcRenderer.on('setCurrency', (evt, currency) => async function init() {
this.state.toggleCurrencies(currency) await setBalances()
) setIsLoading(false)
const newPrizes = await this.fetchAndSetPrices() // listener for touchbar
this.setState({ prices: newPrizes }) global.ipcRenderer.on('setCurrency', (evt, currency) =>
toggleCurrencies(currency)
)
}
await this.setBalances() init()
setInterval(init, ms(refreshInterval))
setInterval(this.fetchAndSetPrices, ms(refreshInterval)) return () => {
setInterval(this.setBalances, ms(refreshInterval)) clearInterval(init)
}
}, [prices])
this.setState({ isLoading: false }) function getAccounts() {
}
getAccounts() {
let accountsPref let accountsPref
const store = process.env.NODE_ENV === 'test' ? new Store() : global.store
if (this.store.has('accounts')) { if (store.has('accounts')) {
accountsPref = this.store.get('accounts') accountsPref = store.get('accounts')
!accountsPref.length ? setNeedsConfig(true) : setNeedsConfig(false)
!accountsPref.length
? this.setState({ needsConfig: true })
: this.setState({ needsConfig: false })
} else { } else {
accountsPref = [] accountsPref = []
this.setState({ needsConfig: true }) setNeedsConfig(true)
} }
return accountsPref return accountsPref
} }
async getBalance(account) { async function setBalances() {
const json = await fetchData(
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest`
)
const balance = json.result / 1e18 // Convert from vodka 10^18
return balance
}
fetchAndSetPrices = async () => {
const currencies = conversions.join(',')
const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true`
)
let newPrices = new Map(this.state.prices) // make a shallow copy of the Map
conversions.map(key => newPrices.set(key, json['ocean-protocol'][key])) // modify the copy
const newPriceChanges = await Object.assign(
...conversions.map(key => ({
[key]: json['ocean-protocol'][key + '_24h_change']
}))
)
global.ipcRenderer.send('prices-updated', Array.from(newPrices)) // convert Map to array, ipc messages seem to kill it
this.setState({ prices: newPrices, priceChanges: newPriceChanges })
return newPrices
}
setBalances = async () => {
const accountsPref = await this.getAccounts()
let newAccounts = [] let newAccounts = []
const accountsPref = await getAccounts()
for (const account of accountsPref) { for (const account of accountsPref) {
const oceanBalance = await this.getBalance(account) const oceanBalance = await getBalance(account)
const conversionsBalance = Object.assign( const conversionsBalance = Object.assign(
...conversions.map(key => ({ ...conversions.map(key => ({
[key]: oceanBalance * this.state.prices.get(key) || 0 [key]: oceanBalance * prices.get(key) || 0
})) }))
) )
@ -129,22 +90,30 @@ export default class AppProvider extends PureComponent {
newAccounts.push(newAccount) newAccounts.push(newAccount)
} }
if (newAccounts !== this.state.accounts) { if (newAccounts !== accounts) {
this.setState({ accounts: newAccounts }) setAccounts(newAccounts)
} }
} }
toggleCurrencies(currency) { function toggleCurrencies(currency) {
const pricesNew = Array.from(this.state.prices) const pricesNew = Array.from(prices)
global.ipcRenderer.send('currency-updated', pricesNew, currency) global.ipcRenderer.send('currency-updated', pricesNew, currency)
this.setState({ currency }) setCurrency(currency)
} }
render() { const context = {
return ( isLoading,
<AppContext.Provider value={this.state}> accounts,
{this.props.children} currency,
</AppContext.Provider> needsConfig,
) accentColor,
toggleCurrencies: currency => toggleCurrencies(currency),
setBalances: () => setBalances()
} }
return <AppContext.Provider value={context}>{children}</AppContext.Provider>
}
AppProvider.propTypes = {
children: PropTypes.any.isRequired
} }

View File

@ -0,0 +1,67 @@
import React, { useEffect, useState } from 'react'
import PropTypes from 'prop-types'
import ms from 'ms'
import { PriceContext } from './createContext'
import { fetchData } from '../../utils'
import { refreshInterval, conversions } from '../../config'
export default function PriceProvider({ children }) {
// construct initial prices Map to get consistent
// order for Ticker and Touchbar
let pricesMap = new Map()
pricesMap.set('ocean', 1)
conversions.map(key => pricesMap.set(key, 0))
const [prices, setPrices] = useState(pricesMap)
const [priceChanges, setPriceChanges] = useState(
Object.assign(
...conversions.map(key => ({
[key]: 0
}))
)
)
async function fetchAndSetPrices() {
const currencies = conversions.join(',')
const json = await fetchData(
`https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true`
)
let newPrices = new Map(prices) // make a shallow copy of the Map
conversions.map(key => newPrices.set(key, json['ocean-protocol'][key])) // modify the copy
const newPriceChanges = await Object.assign(
...conversions.map(key => ({
[key]: json['ocean-protocol'][key + '_24h_change']
}))
)
return { newPrices, newPriceChanges }
}
useEffect(() => {
async function init() {
const { newPrices, newPriceChanges } = await fetchAndSetPrices()
setPrices(newPrices)
setPriceChanges(newPriceChanges)
global.ipcRenderer.send('prices-updated', Array.from(newPrices)) // convert Map to array, ipc messages seem to kill it
}
init()
setInterval(init, ms(refreshInterval))
return () => {
clearInterval(init)
}
}, [])
return (
<PriceContext.Provider value={{ prices, priceChanges }}>
{children}
</PriceContext.Provider>
)
}
PriceProvider.propTypes = {
children: PropTypes.any.isRequired
}

View File

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

View File

@ -1,20 +1,18 @@
const { app, shell } = require('electron') const { app, shell } = require('electron')
const { formatCurrency } = require('@coingecko/cryptoformat') const { formatCurrency } = require('@coingecko/cryptoformat')
const axios = require('axios')
const fetchData = async url => { const fetchData = async url => {
try { try {
const response = await fetch(url) const response = await axios(url)
if (response.status !== 200) { if (response.status !== 200) {
return console.log('Non-200 response: ' + response.status) // eslint-disable-line return console.error('Non-200 response: ' + response.status)
} }
const json = await response.json() return response.data
if (!json) return
return json
} catch (error) { } catch (error) {
console.log('Error parsing json:' + error) // eslint-disable-line console.error('Error parsing json: ' + error.message)
} }
} }

View File

@ -1,10 +1,11 @@
import React from 'react' import React from 'react'
import { render } from '@testing-library/react' import { render, wait } from '@testing-library/react'
import Layout from '../src/renderer/Layout' import Layout from '../src/renderer/Layout'
describe('Layout', () => { describe('Layout', () => {
it('renders correctly', () => { it('renders correctly', async () => {
const { container } = render(<Layout>Hello</Layout>) const { container } = render(<Layout>Hello</Layout>)
await wait()
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
}) })
}) })

View File

@ -12,7 +12,12 @@ const priceChanges = {
eth: -17.538786176215627 eth: -17.538786176215627
} }
export default { export const priceContext = {
prices,
priceChanges
}
export const appContext = {
accentColor: '#0a5fff', accentColor: '#0a5fff',
accounts: [ accounts: [
{ {
@ -28,7 +33,5 @@ export default {
], ],
currency: 'ocean', currency: 'ocean',
isLoading: false, isLoading: false,
needsConfig: false, needsConfig: false
prices,
priceChanges
} }

View File

@ -11,6 +11,10 @@ const remote = {
getCurrentWindow: jest.fn() getCurrentWindow: jest.fn()
} }
const ipcRenderer = {
on: jest.fn()
}
// for the shell module above // for the shell module above
const shell = { const shell = {
openExternal: jest.fn() openExternal: jest.fn()
@ -19,5 +23,6 @@ const shell = {
module.exports = { module.exports = {
electron, electron,
remote, remote,
shell shell,
ipcRenderer
} }

View File

@ -0,0 +1,3 @@
module.exports = {
has: () => jest.fn()
}

10
tests/__mocks__/global.js Normal file
View File

@ -0,0 +1,10 @@
const global = {
ipcRenderer: {
on: () => jest.fn()
},
store: {
has: () => jest.fn()
}
}
module.exports = global

View File

@ -19,7 +19,7 @@ module.exports = {
'<rootDir>/coverage' '<rootDir>/coverage'
], ],
testURL: 'http://localhost', testURL: 'http://localhost',
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], setupFilesAfterEnv: ['<rootDir>/tests/setupTests.js'],
runner: '@jest-runner/electron', runner: '@jest-runner/electron',
testEnvironment: '@jest-runner/electron/environment', testEnvironment: '@jest-runner/electron/environment',
coverageDirectory: '../../coverage/', coverageDirectory: '../../coverage/',

View File

@ -1,15 +1,20 @@
import React from 'react' import React from 'react'
import { render, fireEvent } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { AppContext } from '../../src/renderer/store/createContext' import {
import context from '../__fixtures__/context' AppContext,
PriceContext
} from '../../src/renderer/store/createContext'
import { appContext, priceContext } from '../__fixtures__/context'
import Home from '../../src/renderer/pages/index' import Home from '../../src/renderer/pages/index'
describe('Home', () => { describe('Home', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { container, getByText } = render( const { container, getByText } = render(
<AppContext.Provider value={context}> <PriceContext.Provider value={priceContext}>
<Home /> <AppContext.Provider value={appContext}>
</AppContext.Provider> <Home />
</AppContext.Provider>
</PriceContext.Provider>
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()
fireEvent.click(getByText(/Ξ/)) fireEvent.click(getByText(/Ξ/))
@ -18,9 +23,11 @@ describe('Home', () => {
it('renders Welcome without config', () => { it('renders Welcome without config', () => {
const { container } = render( const { container } = render(
<AppContext.Provider value={{ ...context, needsConfig: true }}> <PriceContext.Provider value={priceContext}>
<Home /> <AppContext.Provider value={{ ...appContext, needsConfig: true }}>
</AppContext.Provider> <Home />
</AppContext.Provider>
</PriceContext.Provider>
) )
expect(container.firstChild).toHaveTextContent( expect(container.firstChild).toHaveTextContent(
'Add your first address to get started.' 'Add your first address to get started.'

3
tests/setupTests.js Normal file
View File

@ -0,0 +1,3 @@
import '@testing-library/jest-dom/extend-expect'
jest.mock('electron-store')