feat: add loading state to wallet balance (#508)

* feat: add loading state to wallet balance

* refactor: extract the wallet balance into a provider
This commit is contained in:
Vojtech Simetka
2022-08-03 14:09:24 +02:00
committed by GitHub
parent a7bd94af82
commit b9c008f019
10 changed files with 150 additions and 59 deletions
+21 -18
View File
@@ -13,6 +13,7 @@ import { Provider as PlatformProvider } from './providers/Platform'
import { Provider as SettingsProvider } from './providers/Settings' import { Provider as SettingsProvider } from './providers/Settings'
import { Provider as StampsProvider } from './providers/Stamps' import { Provider as StampsProvider } from './providers/Stamps'
import { Provider as TopUpProvider } from './providers/TopUp' import { Provider as TopUpProvider } from './providers/TopUp'
import { Provider as BalanceProvider } from './providers/WalletBalance'
import BaseRouter from './routes' import BaseRouter from './routes'
import { theme } from './theme' import { theme } from './theme'
import { config } from './config' import { config } from './config'
@@ -43,24 +44,26 @@ const App = ({ beeApiUrl, beeDebugApiUrl, lockedApiSettings, isBeeDesktop }: Pro
> >
<TopUpProvider> <TopUpProvider>
<BeeProvider> <BeeProvider>
<StampsProvider> <BalanceProvider>
<FileProvider> <StampsProvider>
<FeedsProvider> <FileProvider>
<PlatformProvider> <FeedsProvider>
<SnackbarProvider preventDuplicate anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}> <PlatformProvider>
<Router> <SnackbarProvider preventDuplicate anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}>
<> <Router>
<CssBaseline /> <>
<Dashboard> <CssBaseline />
<BaseRouter /> <Dashboard>
</Dashboard> <BaseRouter />
</> </Dashboard>
</Router> </>
</SnackbarProvider> </Router>
</PlatformProvider> </SnackbarProvider>
</FeedsProvider> </PlatformProvider>
</FileProvider> </FeedsProvider>
</StampsProvider> </FileProvider>
</StampsProvider>
</BalanceProvider>
</BeeProvider> </BeeProvider>
</TopUpProvider> </TopUpProvider>
</SettingsProvider> </SettingsProvider>
+3 -1
View File
@@ -13,13 +13,15 @@ import { SwarmButton } from '../../../components/SwarmButton'
import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard' import TroubleshootConnectionCard from '../../../components/TroubleshootConnectionCard'
import { CheckState, Context as BeeContext } from '../../../providers/Bee' import { CheckState, Context as BeeContext } from '../../../providers/Bee'
import { Context as SettingsContext } from '../../../providers/Settings' import { Context as SettingsContext } from '../../../providers/Settings'
import { Context as BalanceProvider } from '../../../providers/WalletBalance'
import { ROUTES } from '../../../routes' import { ROUTES } from '../../../routes'
import { AccountNavigation } from '../AccountNavigation' import { AccountNavigation } from '../AccountNavigation'
import { Header } from '../Header' import { Header } from '../Header'
export function AccountWallet(): ReactElement { export function AccountWallet(): ReactElement {
const { balance, nodeAddresses, nodeInfo, status } = useContext(BeeContext) const { nodeAddresses, nodeInfo, status } = useContext(BeeContext)
const { isBeeDesktop } = useContext(SettingsContext) const { isBeeDesktop } = useContext(SettingsContext)
const { balance } = useContext(BalanceProvider)
const navigate = useNavigate() const navigate = useNavigate()
+2 -2
View File
@@ -11,7 +11,7 @@ import ExpandableListItemKey from '../../components/ExpandableListItemKey'
import { HistoryHeader } from '../../components/HistoryHeader' import { HistoryHeader } from '../../components/HistoryHeader'
import { Loading } from '../../components/Loading' import { Loading } from '../../components/Loading'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BalanceProvider } from '../../providers/WalletBalance'
import { Context as TopUpContext } from '../../providers/TopUp' import { Context as TopUpContext } from '../../providers/TopUp'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { createGiftWallet } from '../../utils/desktop' import { createGiftWallet } from '../../utils/desktop'
@@ -24,7 +24,7 @@ const GIFT_WALLET_FUND_BZZ_AMOUNT = Token.fromDecimal('0.5', 16)
export default function Index(): ReactElement { export default function Index(): ReactElement {
const { giftWallets, addGiftWallet } = useContext(TopUpContext) const { giftWallets, addGiftWallet } = useContext(TopUpContext)
const { provider } = useContext(SettingsContext) const { provider } = useContext(SettingsContext)
const { balance } = useContext(BeeContext) const { balance } = useContext(BalanceProvider)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [balances, setBalances] = useState<ResolvedWallet[]>([]) const [balances, setBalances] = useState<ResolvedWallet[]>([])
+15 -5
View File
@@ -6,6 +6,7 @@ import Upload from 'remixicon-react/UploadLineIcon'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context as BalanceProvider } from '../../providers/WalletBalance'
import Card from '../../components/Card' import Card from '../../components/Card'
import Map from '../../components/Map' import Map from '../../components/Map'
import ExpandableListItem from '../../components/ExpandableListItem' import ExpandableListItem from '../../components/ExpandableListItem'
@@ -24,15 +25,24 @@ export default function Status(): ReactElement {
latestBeeVersionUrl, latestBeeVersionUrl,
topology, topology,
nodeInfo, nodeInfo,
balance,
chequebookBalance, chequebookBalance,
wallet, chainId,
} = useContext(BeeContext) } = useContext(BeeContext)
const { isBeeDesktop } = useContext(SettingsContext) const { isBeeDesktop } = useContext(SettingsContext)
const { balance, error } = useContext(BalanceProvider)
const { beeDesktopVersion } = useIsBeeDesktop() const { beeDesktopVersion } = useIsBeeDesktop()
const { newBeeDesktopVersion } = useNewBeeDesktopVersion(isBeeDesktop) const { newBeeDesktopVersion } = useNewBeeDesktopVersion(isBeeDesktop)
const navigate = useNavigate() const navigate = useNavigate()
let balanceText = 'Loading...'
if (error) {
balanceText = 'Could not load...'
console.error(error) // eslint-disable-line
} else if (balance) {
balanceText = `${balance.bzz.toSignificantDigits(4)} xBZZ | ${balance.dai.toSignificantDigits(4)} xDAI`
}
return ( return (
<div> <div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'stretch', alignContent: 'stretch' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'stretch', alignContent: 'stretch' }}>
@@ -46,7 +56,7 @@ export default function Status(): ReactElement {
onClick: () => navigate(ROUTES.ACCOUNT_WALLET), onClick: () => navigate(ROUTES.ACCOUNT_WALLET),
}} }}
icon={<Wallet />} icon={<Wallet />}
title={`${balance?.bzz.toSignificantDigits(4)} xBZZ | ${balance?.dai.toSignificantDigits(4)} xDAI`} title={balanceText}
subtitle="Current wallet balance." subtitle="Current wallet balance."
status="ok" status="ok"
/> />
@@ -89,7 +99,7 @@ export default function Status(): ReactElement {
icon={<ExchangeFunds />} icon={<ExchangeFunds />}
title={ title={
chequebookBalance?.availableBalance chequebookBalance?.availableBalance
? `${chequebookBalance.availableBalance.toFixedDecimal(4)} xBZZ` ? `${chequebookBalance.availableBalance.toSignificantDigits(4)} xBZZ`
: 'No available balance.' : 'No available balance.'
} }
subtitle="Chequebook not setup." subtitle="Chequebook not setup."
@@ -149,7 +159,7 @@ export default function Status(): ReactElement {
} }
/> />
<ExpandableListItem label="Mode" value={nodeInfo?.beeMode} /> <ExpandableListItem label="Mode" value={nodeInfo?.beeMode} />
{wallet && <ExpandableListItem label="Blockchain network" value={chainIdToName(wallet.chainID)} />} {chainId && <ExpandableListItem label="Blockchain network" value={chainIdToName(chainId)} />}
</div> </div>
) )
} }
+3 -1
View File
@@ -9,6 +9,7 @@ import { Loading } from '../../components/Loading'
import { SwarmButton } from '../../components/SwarmButton' import { SwarmButton } from '../../components/SwarmButton'
import { SwarmDivider } from '../../components/SwarmDivider' import { SwarmDivider } from '../../components/SwarmDivider'
import { Context } from '../../providers/Bee' import { Context } from '../../providers/Bee'
import { Context as BalanceProvider } from '../../providers/WalletBalance'
import { TopUpProgressIndicator } from './TopUpProgressIndicator' import { TopUpProgressIndicator } from './TopUpProgressIndicator'
const MINIMUM_XDAI = '0.5' const MINIMUM_XDAI = '0.5'
@@ -21,7 +22,8 @@ interface Props {
} }
export default function Index({ header, title, p, next }: Props): ReactElement { export default function Index({ header, title, p, next }: Props): ReactElement {
const { nodeAddresses, balance } = useContext(Context) const { nodeAddresses } = useContext(Context)
const { balance } = useContext(BalanceProvider)
const navigate = useNavigate() const navigate = useNavigate()
if (!balance || !nodeAddresses) { if (!balance || !nodeAddresses) {
+4 -3
View File
@@ -13,6 +13,7 @@ import { SwarmButton } from '../../components/SwarmButton'
import { SwarmDivider } from '../../components/SwarmDivider' import { SwarmDivider } from '../../components/SwarmDivider'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context as BalanceProvider } from '../../providers/WalletBalance'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { sleepMs } from '../../utils' import { sleepMs } from '../../utils'
import { restartBeeNode, upgradeToLightNode } from '../../utils/desktop' import { restartBeeNode, upgradeToLightNode } from '../../utils/desktop'
@@ -20,9 +21,9 @@ import { ResolvedWallet } from '../../utils/wallet'
import { BeeModes } from '@ethersphere/bee-js' import { BeeModes } from '@ethersphere/bee-js'
export function GiftCardFund(): ReactElement { export function GiftCardFund(): ReactElement {
const { isBeeDesktop } = useContext(SettingsContext) const { nodeAddresses, nodeInfo } = useContext(BeeContext)
const { nodeAddresses, balance, nodeInfo } = useContext(BeeContext) const { isBeeDesktop, provider, providerUrl } = useContext(SettingsContext)
const { provider, providerUrl } = useContext(SettingsContext) const { balance } = useContext(BalanceProvider)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [wallet, setWallet] = useState<ResolvedWallet | null>(null) const [wallet, setWallet] = useState<ResolvedWallet | null>(null)
+4 -3
View File
@@ -16,6 +16,7 @@ import { SwarmDivider } from '../../components/SwarmDivider'
import { SwarmTextInput } from '../../components/SwarmTextInput' import { SwarmTextInput } from '../../components/SwarmTextInput'
import { BzzToken } from '../../models/BzzToken' import { BzzToken } from '../../models/BzzToken'
import { DaiToken } from '../../models/DaiToken' import { DaiToken } from '../../models/DaiToken'
import { Context as BalanceProvider } from '../../providers/WalletBalance'
import { Context as BeeContext } from '../../providers/Bee' import { Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
@@ -36,9 +37,9 @@ export function Swap({ header }: Props): ReactElement {
const [userInputSwap, setUserInputSwap] = useState<string | null>(null) const [userInputSwap, setUserInputSwap] = useState<string | null>(null)
const [price, setPrice] = useState(DaiToken.fromDecimal('0.6', 18)) const [price, setPrice] = useState(DaiToken.fromDecimal('0.6', 18))
const { providerUrl } = useContext(SettingsContext) const { providerUrl, isBeeDesktop } = useContext(SettingsContext)
const { balance, nodeAddresses, nodeInfo } = useContext(BeeContext) const { nodeAddresses, nodeInfo } = useContext(BeeContext)
const { isBeeDesktop } = useContext(SettingsContext) const { balance } = useContext(BalanceProvider)
const navigate = useNavigate() const navigate = useNavigate()
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
+3 -1
View File
@@ -12,6 +12,7 @@ import { SwarmButton } from '../../components/SwarmButton'
import { ROUTES } from '../../routes' import { ROUTES } from '../../routes'
import { CheckState, Context as BeeContext } from '../../providers/Bee' import { CheckState, Context as BeeContext } from '../../providers/Bee'
import { Context as SettingsContext } from '../../providers/Settings' import { Context as SettingsContext } from '../../providers/Settings'
import { Context as BalanceProvider } from '../../providers/WalletBalance'
import { BeeModes } from '@ethersphere/bee-js' import { BeeModes } from '@ethersphere/bee-js'
import { restartBeeNode, upgradeToLightNode } from '../../utils/desktop' import { restartBeeNode, upgradeToLightNode } from '../../utils/desktop'
import { Loading } from '../../components/Loading' import { Loading } from '../../components/Loading'
@@ -39,7 +40,8 @@ export default function TopUp(): ReactElement {
const navigate = useNavigate() const navigate = useNavigate()
const styles = useStyles() const styles = useStyles()
const { isBeeDesktop } = useContext(SettingsContext) const { isBeeDesktop } = useContext(SettingsContext)
const { balance, nodeInfo, status } = useContext(BeeContext) const { nodeInfo, status } = useContext(BeeContext)
const { balance } = useContext(BalanceProvider)
const { providerUrl } = useContext(SettingsContext) const { providerUrl } = useContext(SettingsContext)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const { enqueueSnackbar } = useSnackbar() const { enqueueSnackbar } = useSnackbar()
+7 -25
View File
@@ -8,7 +8,6 @@ import {
NodeInfo, NodeInfo,
Peer, Peer,
Topology, Topology,
WalletBalance,
} from '@ethersphere/bee-js' } from '@ethersphere/bee-js'
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react' import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import semver from 'semver' import semver from 'semver'
@@ -16,7 +15,6 @@ import PackageJson from '../../package.json'
import { useLatestBeeRelease } from '../hooks/apiHooks' import { useLatestBeeRelease } from '../hooks/apiHooks'
import { Token } from '../models/Token' import { Token } from '../models/Token'
import type { Balance, ChequebookBalance, Settlements } from '../types' import type { Balance, ChequebookBalance, Settlements } from '../types'
import { WalletAddress } from '../utils/wallet'
import { Context as SettingsContext } from './Settings' import { Context as SettingsContext } from './Settings'
const REFRESH_WHEN_OK = 30_000 const REFRESH_WHEN_OK = 30_000
@@ -46,7 +44,6 @@ interface Status {
interface ContextInterface { interface ContextInterface {
status: Status status: Status
balance: WalletAddress | null
latestPublishedVersion?: string latestPublishedVersion?: string
latestUserVersion?: string latestUserVersion?: string
latestUserVersionExact?: string latestUserVersionExact?: string
@@ -65,7 +62,7 @@ interface ContextInterface {
peerCheques: LastChequesResponse | null peerCheques: LastChequesResponse | null
settlements: Settlements | null settlements: Settlements | null
chainState: ChainState | null chainState: ChainState | null
wallet: WalletBalance | null chainId: number | null
latestBeeRelease: LatestBeeRelease | null latestBeeRelease: LatestBeeRelease | null
isLoading: boolean isLoading: boolean
lastUpdate: number | null lastUpdate: number | null
@@ -84,7 +81,6 @@ const initialValues: ContextInterface = {
topology: { isEnabled: false, checkState: CheckState.ERROR }, topology: { isEnabled: false, checkState: CheckState.ERROR },
chequebook: { isEnabled: false, checkState: CheckState.ERROR }, chequebook: { isEnabled: false, checkState: CheckState.ERROR },
}, },
balance: null,
latestPublishedVersion: undefined, latestPublishedVersion: undefined,
latestUserVersion: undefined, latestUserVersion: undefined,
latestUserVersionExact: undefined, latestUserVersionExact: undefined,
@@ -103,7 +99,7 @@ const initialValues: ContextInterface = {
peerCheques: null, peerCheques: null,
settlements: null, settlements: null,
chainState: null, chainState: null,
wallet: null, chainId: null,
latestBeeRelease: null, latestBeeRelease: null,
isLoading: true, isLoading: true,
lastUpdate: null, lastUpdate: null,
@@ -191,7 +187,7 @@ function getStatus(
let isRefreshing = false let isRefreshing = false
export function Provider({ children }: Props): ReactElement { export function Provider({ children }: Props): ReactElement {
const { beeApi, beeDebugApi, provider } = useContext(SettingsContext) const { beeApi, beeDebugApi } = useContext(SettingsContext)
const [apiHealth, setApiHealth] = useState<boolean>(false) const [apiHealth, setApiHealth] = useState<boolean>(false)
const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null) const [debugApiHealth, setDebugApiHealth] = useState<Health | null>(null)
const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null) const [nodeAddresses, setNodeAddresses] = useState<NodeAddresses | null>(null)
@@ -204,8 +200,7 @@ export function Provider({ children }: Props): ReactElement {
const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null) const [peerCheques, setPeerCheques] = useState<LastChequesResponse | null>(null)
const [settlements, setSettlements] = useState<Settlements | null>(null) const [settlements, setSettlements] = useState<Settlements | null>(null)
const [chainState, setChainState] = useState<ChainState | null>(null) const [chainState, setChainState] = useState<ChainState | null>(null)
const [walletAddress, setWalletAddress] = useState<WalletAddress | null>(initialValues.balance) const [chainId, setChainId] = useState<number | null>(null)
const [wallet, setWallet] = useState<WalletBalance | null>(null)
const { latestBeeRelease } = useLatestBeeRelease() const { latestBeeRelease } = useLatestBeeRelease()
@@ -244,18 +239,6 @@ export function Provider({ children }: Props): ReactElement {
if (beeDebugApi !== null) refresh() if (beeDebugApi !== null) refresh()
}, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps }, [beeDebugApi]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (nodeAddresses?.ethereum && provider) {
WalletAddress.make(nodeAddresses.ethereum, provider).then(setWalletAddress)
}
}, [nodeAddresses, provider])
useEffect(() => {
const interval = setInterval(() => walletAddress?.refresh().then(setWalletAddress), REFRESH_WHEN_OK)
return () => clearInterval(interval)
}, [walletAddress])
const refresh = async () => { const refresh = async () => {
// Don't want to refresh when already refreshing // Don't want to refresh when already refreshing
if (isRefreshing) return if (isRefreshing) return
@@ -361,8 +344,8 @@ export function Provider({ children }: Props): ReactElement {
// Wallet // Wallet
beeDebugApi beeDebugApi
.getWalletBalance({ timeout: TIMEOUT }) .getWalletBalance({ timeout: TIMEOUT })
.then(setWallet) .then(({ chainID }) => setChainId(chainID))
.catch(() => setWallet(null)), .catch(() => setChainId(null)),
// Chequebook balance // Chequebook balance
chequeBalanceWrapper() chequeBalanceWrapper()
@@ -429,7 +412,6 @@ export function Provider({ children }: Props): ReactElement {
<Context.Provider <Context.Provider
value={{ value={{
status, status,
balance: walletAddress,
latestUserVersion, latestUserVersion,
latestUserVersionExact, latestUserVersionExact,
latestPublishedVersion, latestPublishedVersion,
@@ -454,7 +436,7 @@ export function Provider({ children }: Props): ReactElement {
peerCheques, peerCheques,
settlements, settlements,
chainState, chainState,
wallet, chainId,
latestBeeRelease, latestBeeRelease,
isLoading, isLoading,
lastUpdate, lastUpdate,
+88
View File
@@ -0,0 +1,88 @@
import { createContext, ReactChild, ReactElement, useContext, useEffect, useState } from 'react'
import { Context as SettingsContext } from './Settings'
import { Context as BeeContext } from './Bee'
import { WalletAddress } from '../utils/wallet'
interface ContextInterface {
balance: WalletAddress | null
error: Error | null
isLoading: boolean
lastUpdate: number | null
start: (frequency?: number) => void
stop: () => void
refresh: () => Promise<void>
}
const initialValues: ContextInterface = {
balance: null,
error: null,
isLoading: false,
lastUpdate: null,
start: () => {}, // eslint-disable-line
stop: () => {}, // eslint-disable-line
refresh: () => Promise.reject(),
}
export const Context = createContext<ContextInterface>(initialValues)
export const Consumer = Context.Consumer
interface Props {
children: ReactChild
}
export function Provider({ children }: Props): ReactElement {
const { provider } = useContext(SettingsContext)
const { nodeAddresses } = useContext(BeeContext)
const [balance, setBalance] = useState<WalletAddress | null>(initialValues.balance)
const [error, setError] = useState<Error | null>(initialValues.error)
const [isLoading, setIsLoading] = useState<boolean>(initialValues.isLoading)
const [lastUpdate, setLastUpdate] = useState<number | null>(initialValues.lastUpdate)
const [frequency, setFrequency] = useState<number | null>(null)
useEffect(() => {
if (nodeAddresses?.ethereum && provider) {
WalletAddress.make(nodeAddresses.ethereum, provider).then(setBalance)
} else {
setBalance(null)
}
}, [nodeAddresses, provider])
const refresh = async () => {
// Don't want to refresh when already refreshing
if (isLoading) return
if (!balance) return
try {
setIsLoading(true)
setBalance(await balance.refresh())
setLastUpdate(Date.now())
} catch (e) {
setError(e as Error)
} finally {
setIsLoading(false)
}
}
const start = (freq = 30000) => setFrequency(freq)
const stop = () => setFrequency(null)
// Start the update loop
useEffect(() => {
refresh()
// Start autorefresh only if the frequency is set
if (frequency) {
const interval = setInterval(refresh, frequency)
return () => clearInterval(interval)
}
}, [frequency]) // eslint-disable-line react-hooks/exhaustive-deps
return (
<Context.Provider value={{ balance, error, isLoading, lastUpdate, start, stop, refresh }}>
{children}
</Context.Provider>
)
}