mirror of
https://github.com/kremalicious/blowfish.git
synced 2024-12-26 22:57:51 +01:00
provider splitup and refactor
This commit is contained in:
parent
c3edae6280
commit
2efee0ec73
16
package.json
16
package.json
@ -30,11 +30,13 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@coingecko/cryptoformat": "^0.3.4",
|
||||
"axios": "^0.19.2",
|
||||
"electron-is-dev": "^1.1.0",
|
||||
"electron-next": "^3.1.5",
|
||||
"electron-store": "^5.1.0",
|
||||
"electron-store": "^5.1.1",
|
||||
"ethereum-address": "^0.0.4",
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"ethjs-unit": "^0.1.6",
|
||||
"ms": "^2.1.2",
|
||||
"shortid": "^2.2.15"
|
||||
},
|
||||
@ -43,15 +45,15 @@
|
||||
"@babel/preset-env": "^7.8.4",
|
||||
"@jest-runner/electron": "^2.0.3",
|
||||
"@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/react": "^9.4.0",
|
||||
"@testing-library/react": "^9.4.1",
|
||||
"auto-changelog": "^1.16.2",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^25.1.0",
|
||||
"copy": "^0.3.2",
|
||||
"cross-env": "^7.0.0",
|
||||
"electron": "^8.0.0",
|
||||
"electron": "^8.0.1",
|
||||
"electron-builder": "^22.3.2",
|
||||
"electron-devtools-installer": "^2.2.4",
|
||||
"eslint": "^6.8.0",
|
||||
@ -59,14 +61,14 @@
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^25.1.0",
|
||||
"next": "^9.2.1",
|
||||
"next": "^9.2.2",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier-stylelint": "^0.4.2",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-pose": "^4.0.10",
|
||||
"release-it": "^12.4.3",
|
||||
"stylelint": "^13.1.0",
|
||||
"release-it": "^12.6.1",
|
||||
"stylelint": "^13.2.0",
|
||||
"stylelint-config-css-modules": "^2.2.0",
|
||||
"stylelint-config-standard": "^20.0.0"
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
|
||||
import posed, { PoseGroup } from 'react-pose'
|
||||
import shortid from 'shortid'
|
||||
import AppProvider from './store/AppProvider'
|
||||
import PriceProvider from './store/PriceProvider'
|
||||
import { defaultAnimation } from './components/Animations'
|
||||
import Titlebar from './components/Titlebar'
|
||||
import styles from './Layout.module.css'
|
||||
@ -11,14 +12,16 @@ const Animation = posed.div(defaultAnimation)
|
||||
|
||||
export default function Layout({ children }) {
|
||||
return (
|
||||
<AppProvider>
|
||||
{process.platform === 'darwin' && <Titlebar />}
|
||||
<div className={styles.app}>
|
||||
<PoseGroup animateOnMount>
|
||||
<Animation key={shortid.generate()}>{children}</Animation>
|
||||
</PoseGroup>
|
||||
</div>
|
||||
</AppProvider>
|
||||
<PriceProvider>
|
||||
<AppProvider>
|
||||
{process.platform === 'darwin' && <Titlebar />}
|
||||
<div className={styles.app}>
|
||||
<PoseGroup animateOnMount>
|
||||
<Animation key={shortid.generate()}>{children}</Animation>
|
||||
</PoseGroup>
|
||||
</div>
|
||||
</AppProvider>
|
||||
</PriceProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useContext } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import posed, { PoseGroup } from 'react-pose'
|
||||
import { AppContext } from '../../store/createContext'
|
||||
import { AppContext, PriceContext } from '../../store/createContext'
|
||||
import { cryptoFormatter } from '../../../utils'
|
||||
import stylesIndex from '../../pages/index.module.css'
|
||||
import styles from './Ticker.module.css'
|
||||
@ -10,7 +10,7 @@ import { fadeIn } from '../Animations'
|
||||
const Item = posed.div(fadeIn)
|
||||
|
||||
const Change = ({ currency }) => {
|
||||
const { priceChanges } = useContext(AppContext)
|
||||
const { priceChanges } = useContext(PriceContext)
|
||||
const isNegative = JSON.stringify(priceChanges[currency]).startsWith('-')
|
||||
let classes = isNegative ? styles.negative : styles.positive
|
||||
|
||||
@ -27,13 +27,10 @@ Change.propTypes = {
|
||||
}
|
||||
|
||||
const Items = () => {
|
||||
const {
|
||||
prices,
|
||||
needsConfig,
|
||||
currency,
|
||||
toggleCurrencies,
|
||||
accentColor
|
||||
} = useContext(AppContext)
|
||||
const { prices } = useContext(PriceContext)
|
||||
const { needsConfig, currency, toggleCurrencies, accentColor } = useContext(
|
||||
AppContext
|
||||
)
|
||||
|
||||
const activeStyle = {
|
||||
backgroundColor: accentColor,
|
||||
|
@ -2,9 +2,9 @@ import React, { useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Router from 'next/router'
|
||||
// import { ipcRenderer } from 'electron'
|
||||
import Layout from '../Layout'
|
||||
|
||||
import '../global.css'
|
||||
import Layout from '../Layout'
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
useEffect(() => {
|
||||
|
@ -1,120 +1,81 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import React, { useContext, useState, useEffect } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import ms from 'ms'
|
||||
// import { ipcRenderer } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import { AppContext } from './createContext'
|
||||
import unit from 'ethjs-unit'
|
||||
import { AppContext, PriceContext } from './createContext'
|
||||
import { fetchData } from '../../utils'
|
||||
import { refreshInterval, conversions, oceanTokenContract } from '../../config'
|
||||
|
||||
// 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))
|
||||
async function getBalance(account) {
|
||||
const json = await fetchData(
|
||||
`https://api.etherscan.io/api?module=account&action=tokenbalance&contractaddress=${oceanTokenContract}&address=${account}&tag=latest`
|
||||
)
|
||||
|
||||
export default class AppProvider extends PureComponent {
|
||||
static propTypes = {
|
||||
children: PropTypes.any.isRequired
|
||||
}
|
||||
const balance = unit.fromWei(`${json.result}`, 'ether')
|
||||
return balance
|
||||
}
|
||||
|
||||
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 = {
|
||||
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() {
|
||||
useEffect(() => {
|
||||
// listener for accent color
|
||||
global.ipcRenderer.on('accent-color', (evt, accentColor) => {
|
||||
this.setState({ accentColor })
|
||||
setAccentColor(accentColor)
|
||||
})
|
||||
}, [])
|
||||
|
||||
// listener for touchbar
|
||||
global.ipcRenderer.on('setCurrency', (evt, currency) =>
|
||||
this.state.toggleCurrencies(currency)
|
||||
)
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
await setBalances()
|
||||
setIsLoading(false)
|
||||
|
||||
const newPrizes = await this.fetchAndSetPrices()
|
||||
this.setState({ prices: newPrizes })
|
||||
// listener for touchbar
|
||||
global.ipcRenderer.on('setCurrency', (evt, currency) =>
|
||||
toggleCurrencies(currency)
|
||||
)
|
||||
}
|
||||
|
||||
await this.setBalances()
|
||||
init()
|
||||
setInterval(init, ms(refreshInterval))
|
||||
|
||||
setInterval(this.fetchAndSetPrices, ms(refreshInterval))
|
||||
setInterval(this.setBalances, ms(refreshInterval))
|
||||
return () => {
|
||||
clearInterval(init)
|
||||
}
|
||||
}, [prices])
|
||||
|
||||
this.setState({ isLoading: false })
|
||||
}
|
||||
|
||||
getAccounts() {
|
||||
function getAccounts() {
|
||||
let accountsPref
|
||||
const store = process.env.NODE_ENV === 'test' ? new Store() : global.store
|
||||
|
||||
if (this.store.has('accounts')) {
|
||||
accountsPref = this.store.get('accounts')
|
||||
|
||||
!accountsPref.length
|
||||
? this.setState({ needsConfig: true })
|
||||
: this.setState({ needsConfig: false })
|
||||
if (store.has('accounts')) {
|
||||
accountsPref = store.get('accounts')
|
||||
!accountsPref.length ? setNeedsConfig(true) : setNeedsConfig(false)
|
||||
} else {
|
||||
accountsPref = []
|
||||
this.setState({ needsConfig: true })
|
||||
setNeedsConfig(true)
|
||||
}
|
||||
|
||||
return accountsPref
|
||||
}
|
||||
|
||||
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 / 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()
|
||||
|
||||
async function setBalances() {
|
||||
let newAccounts = []
|
||||
const accountsPref = await getAccounts()
|
||||
|
||||
for (const account of accountsPref) {
|
||||
const oceanBalance = await this.getBalance(account)
|
||||
const oceanBalance = await getBalance(account)
|
||||
|
||||
const conversionsBalance = Object.assign(
|
||||
...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)
|
||||
}
|
||||
|
||||
if (newAccounts !== this.state.accounts) {
|
||||
this.setState({ accounts: newAccounts })
|
||||
if (newAccounts !== accounts) {
|
||||
setAccounts(newAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
toggleCurrencies(currency) {
|
||||
const pricesNew = Array.from(this.state.prices)
|
||||
function toggleCurrencies(currency) {
|
||||
const pricesNew = Array.from(prices)
|
||||
global.ipcRenderer.send('currency-updated', pricesNew, currency)
|
||||
this.setState({ currency })
|
||||
setCurrency(currency)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AppContext.Provider value={this.state}>
|
||||
{this.props.children}
|
||||
</AppContext.Provider>
|
||||
)
|
||||
const context = {
|
||||
isLoading,
|
||||
accounts,
|
||||
currency,
|
||||
needsConfig,
|
||||
accentColor,
|
||||
toggleCurrencies: currency => toggleCurrencies(currency),
|
||||
setBalances: () => setBalances()
|
||||
}
|
||||
|
||||
return <AppContext.Provider value={context}>{children}</AppContext.Provider>
|
||||
}
|
||||
|
||||
AppProvider.propTypes = {
|
||||
children: PropTypes.any.isRequired
|
||||
}
|
||||
|
67
src/renderer/store/PriceProvider.jsx
Normal file
67
src/renderer/store/PriceProvider.jsx
Normal 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
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
const AppContext = createContext()
|
||||
const PriceContext = createContext()
|
||||
|
||||
export { AppContext }
|
||||
export { AppContext, PriceContext }
|
||||
|
12
src/utils.js
12
src/utils.js
@ -1,20 +1,18 @@
|
||||
const { app, shell } = require('electron')
|
||||
const { formatCurrency } = require('@coingecko/cryptoformat')
|
||||
const axios = require('axios')
|
||||
|
||||
const fetchData = async url => {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const response = await axios(url)
|
||||
|
||||
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()
|
||||
if (!json) return
|
||||
|
||||
return json
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.log('Error parsing json:' + error) // eslint-disable-line
|
||||
console.error('Error parsing json: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { render, wait } from '@testing-library/react'
|
||||
import Layout from '../src/renderer/Layout'
|
||||
|
||||
describe('Layout', () => {
|
||||
it('renders correctly', () => {
|
||||
it('renders correctly', async () => {
|
||||
const { container } = render(<Layout>Hello</Layout>)
|
||||
await wait()
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
@ -12,7 +12,12 @@ const priceChanges = {
|
||||
eth: -17.538786176215627
|
||||
}
|
||||
|
||||
export default {
|
||||
export const priceContext = {
|
||||
prices,
|
||||
priceChanges
|
||||
}
|
||||
|
||||
export const appContext = {
|
||||
accentColor: '#0a5fff',
|
||||
accounts: [
|
||||
{
|
||||
@ -28,7 +33,5 @@ export default {
|
||||
],
|
||||
currency: 'ocean',
|
||||
isLoading: false,
|
||||
needsConfig: false,
|
||||
prices,
|
||||
priceChanges
|
||||
needsConfig: false
|
||||
}
|
||||
|
@ -11,6 +11,10 @@ const remote = {
|
||||
getCurrentWindow: jest.fn()
|
||||
}
|
||||
|
||||
const ipcRenderer = {
|
||||
on: jest.fn()
|
||||
}
|
||||
|
||||
// for the shell module above
|
||||
const shell = {
|
||||
openExternal: jest.fn()
|
||||
@ -19,5 +23,6 @@ const shell = {
|
||||
module.exports = {
|
||||
electron,
|
||||
remote,
|
||||
shell
|
||||
shell,
|
||||
ipcRenderer
|
||||
}
|
||||
|
3
tests/__mocks__/electronStore.js
Normal file
3
tests/__mocks__/electronStore.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
has: () => jest.fn()
|
||||
}
|
10
tests/__mocks__/global.js
Normal file
10
tests/__mocks__/global.js
Normal file
@ -0,0 +1,10 @@
|
||||
const global = {
|
||||
ipcRenderer: {
|
||||
on: () => jest.fn()
|
||||
},
|
||||
store: {
|
||||
has: () => jest.fn()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = global
|
@ -19,7 +19,7 @@ module.exports = {
|
||||
'<rootDir>/coverage'
|
||||
],
|
||||
testURL: 'http://localhost',
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/setupTests.js'],
|
||||
runner: '@jest-runner/electron',
|
||||
testEnvironment: '@jest-runner/electron/environment',
|
||||
coverageDirectory: '../../coverage/',
|
||||
|
@ -1,15 +1,20 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { AppContext } from '../../src/renderer/store/createContext'
|
||||
import context from '../__fixtures__/context'
|
||||
import {
|
||||
AppContext,
|
||||
PriceContext
|
||||
} from '../../src/renderer/store/createContext'
|
||||
import { appContext, priceContext } from '../__fixtures__/context'
|
||||
import Home from '../../src/renderer/pages/index'
|
||||
|
||||
describe('Home', () => {
|
||||
it('renders correctly', () => {
|
||||
const { container, getByText } = render(
|
||||
<AppContext.Provider value={context}>
|
||||
<Home />
|
||||
</AppContext.Provider>
|
||||
<PriceContext.Provider value={priceContext}>
|
||||
<AppContext.Provider value={appContext}>
|
||||
<Home />
|
||||
</AppContext.Provider>
|
||||
</PriceContext.Provider>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
fireEvent.click(getByText(/Ξ/))
|
||||
@ -18,9 +23,11 @@ describe('Home', () => {
|
||||
|
||||
it('renders Welcome without config', () => {
|
||||
const { container } = render(
|
||||
<AppContext.Provider value={{ ...context, needsConfig: true }}>
|
||||
<Home />
|
||||
</AppContext.Provider>
|
||||
<PriceContext.Provider value={priceContext}>
|
||||
<AppContext.Provider value={{ ...appContext, needsConfig: true }}>
|
||||
<Home />
|
||||
</AppContext.Provider>
|
||||
</PriceContext.Provider>
|
||||
)
|
||||
expect(container.firstChild).toHaveTextContent(
|
||||
'Add your first address to get started.'
|
||||
|
3
tests/setupTests.js
Normal file
3
tests/setupTests.js
Normal file
@ -0,0 +1,3 @@
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
|
||||
jest.mock('electron-store')
|
Loading…
Reference in New Issue
Block a user